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
+14 -7
View File
@@ -14,13 +14,20 @@ import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.sv
import styles from "./styles.module.css"; import styles from "./styles.module.css";
export type AccordionVariant =
| "default"
| "text"
| "paper"
| "minimal"
| "solid";
export interface AccordionProps extends HTMLAttributes<HTMLElement> { export interface AccordionProps extends HTMLAttributes<HTMLElement> {
variant?: "default" | "text" | "paper"; variant?: AccordionVariant;
summary: ReactNode; summary: ReactNode;
description?: ReactNode; description?: ReactNode;
size?: "small" | "medium"; size?: "xx-small" | "x-small" | "small" | "medium";
forceShow?: boolean; forceShow?: boolean;
useTheme?: boolean; themed?: boolean;
resetOpen?: boolean; resetOpen?: boolean;
override?: boolean | null; override?: boolean | null;
handleIsOpen?: (id: string, isOpen: boolean) => void; handleIsOpen?: (id: string, isOpen: boolean) => void;
@@ -40,8 +47,7 @@ export const Accordion: FC<AccordionProps> = ({
variant = "default", variant = "default",
children, children,
forceShow = false, forceShow = false,
// style, themed,
useTheme,
resetOpen, resetOpen,
handleIsOpen, handleIsOpen,
override = null, override = null,
@@ -101,7 +107,7 @@ export const Accordion: FC<AccordionProps> = ({
styles.details, styles.details,
styles[size], styles[size],
styles[variant], styles[variant],
useTheme && styles.theme, themed && styles.themed,
showAlert && styles.alert, showAlert && styles.alert,
className, className,
])} ])}
@@ -113,6 +119,7 @@ export const Accordion: FC<AccordionProps> = ({
className={styles.summary} className={styles.summary}
onClick={handleToggle} onClick={handleToggle}
ref={summaryRef} ref={summaryRef}
data-testid="accordion-summary"
> >
{summaryImage && ( {summaryImage && (
<img <img
@@ -123,7 +130,7 @@ export const Accordion: FC<AccordionProps> = ({
/> />
)} )}
<div className={styles.heading}> <div className={styles.heading}>
<div> <div className={styles.summaryContent}>
{summary} {summary}
{summaryMetaItems && ( {summaryMetaItems && (
<div className={styles.metaItems}> <div className={styles.metaItems}>
@@ -0,0 +1,41 @@
import clsx from "clsx";
import { FC, HTMLAttributes, ReactNode } from "react";
import styles from "./styles.module.css";
export interface AccordionSummaryProps
extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
imageUrl?: string;
title: ReactNode;
description?: string | string[];
callout?: ReactNode;
}
export const AccordionSummary: FC<AccordionSummaryProps> = ({
callout,
className,
description,
imageUrl,
title,
...props
}) => {
const buildDescription = () => {
if (Array.isArray(description)) {
return description.join(" • ");
}
return description;
};
return (
<div className={clsx([styles.accordionSummary, className])} {...props}>
{imageUrl && <img className={styles.avatar} src={imageUrl} alt="" />}
<div className={styles.text}>
<div className={styles.title}>{title}</div>
{description && (
<p className={styles.description}>{buildDescription()}</p>
)}
</div>
{callout && <div className={styles.callout}>{callout}</div>}
</div>
);
};
+11 -2
View File
@@ -1,5 +1,6 @@
import clsx from "clsx"; import clsx from "clsx";
import { FC } from "react"; import Color from "color";
import { CSSProperties, FC } from "react";
import { import {
Button as TtuiButton, Button as TtuiButton,
@@ -33,7 +34,14 @@ export const Button: FC<ButtonProps> = ({
const isCustomSize = size === "xx-small"; const isCustomSize = size === "xx-small";
const isCustomVariant = variant === "builder" || variant === "builder-text"; const isCustomVariant = variant === "builder" || variant === "builder-text";
const isCustomColor = color === "builder-green"; const isCustomColor = color === "builder-green";
const { isDarkMode } = useCharacterTheme(); const { isDarkMode, themeColor } = useCharacterTheme();
let overrideStyles: CSSProperties = {};
if (isDarkMode && themed && variant === "solid" && size === "xx-small") {
overrideStyles = {
// Determine if the theme color is darker or lighter
color: Color(themeColor).isDark() ? "" : "black",
};
}
return ( return (
<TtuiButton <TtuiButton
@@ -53,6 +61,7 @@ export const Button: FC<ButtonProps> = ({
variant={!isCustomVariant ? variant : undefined} variant={!isCustomVariant ? variant : undefined}
// Children are included in props // Children are included in props
color={!isCustomColor ? color : undefined} color={!isCustomColor ? color : undefined}
style={overrideStyles}
{...props} {...props}
/> />
); );
+17 -17
View File
@@ -12,8 +12,7 @@ interface CheckboxProps extends TtuiCheckboxProps {
themed?: boolean; themed?: boolean;
darkMode?: boolean; darkMode?: boolean;
variant?: "default" | "builder" | "sidebar"; variant?: "default" | "builder" | "sidebar";
onClick?: (isEnabled: boolean) => void; onChangeConfirm?: (
onChangePromise?: (
newIsEnabled: boolean, newIsEnabled: boolean,
accept: () => void, accept: () => void,
reject: () => void reject: () => void
@@ -26,51 +25,52 @@ export const Checkbox: FC<CheckboxProps> = ({
variant = "default", variant = "default",
darkMode, darkMode,
checked = false, checked = false,
onChangePromise, onChangeConfirm,
onClick, onChange,
disabled, disabled,
...props ...props
}) => { }) => {
const [isChecked, setIsChecked] = useState(checked); const [isChecked, setIsChecked] = useState(checked);
useEffect(() => {
setIsChecked(checked);
}, [checked]);
const handleChange = (evt: ChangeEvent<HTMLInputElement>): void => { const handleChange = (evt: ChangeEvent<HTMLInputElement>): void => {
evt.stopPropagation(); evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation(); evt.nativeEvent.stopImmediatePropagation();
const newCheckedState = !isChecked; const newCheckedState = evt.target.checked;
// promise-based logic // promise-based logic
if (onChangePromise) { if (onChangeConfirm) {
onChangePromise( onChangeConfirm(
newCheckedState, newCheckedState,
() => { () => {
// Promise accepted, update the state // Promise accepted, update the state
setIsChecked(newCheckedState); setIsChecked(newCheckedState);
}, },
() => { () => {
// Promise rejected, do nothing // Promise rejected, reset the checkbox to its previous state
setIsChecked(!newCheckedState);
} }
); );
} else { } else {
// Update the state immediately if no promise handling - only IF onClick is provided // Update the state immediately if no promise handling - only IF onClick is provided
if (onClick) { if (onChange) {
onClick(newCheckedState); onChange(evt);
setIsChecked(newCheckedState); setIsChecked(newCheckedState);
} }
} }
}; };
useEffect(() => {
setIsChecked(checked);
}, [checked]);
return ( return (
<TtuiCheckbox <TtuiCheckbox
className={clsx([ className={clsx([
styles.checkbox,
styles[variant],
themed && styles.themed, themed && styles.themed,
darkMode && styles[darkMode ? "darkMode" : "lightMode"],
className, className,
variant === "default" && styles.default,
variant === "builder" && styles.builder,
])} ])}
onChange={handleChange} onChange={handleChange}
aria-pressed={isChecked} aria-pressed={isChecked}
@@ -0,0 +1,195 @@
import clsx from "clsx";
import { orderBy } from "lodash";
import { ChangeEvent, FC, HTMLAttributes, ReactNode } from "react";
import { Accordion, AccordionProps } from "../Accordion";
import { Checkbox } from "../Checkbox";
import styles from "./styles.module.css";
export interface CheckboxInfo {
description?: string;
disabled?: boolean;
initiallyEnabled: boolean;
label: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
onChangeConfirm?: (
newIsEnabled: boolean,
accept: () => void,
reject: () => void
) => void;
sortOrder?: number;
value?: string | number;
}
interface CheckboxGroupProps
extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
accordionDefaultOpen?: boolean;
accordionSummary?: string;
checkboxes: Array<CheckboxInfo>;
disabled?: boolean;
description?: ReactNode;
name: string;
onToggleAll?: (e: ChangeEvent<HTMLInputElement>) => void;
showAccordion?: boolean;
themed?: boolean;
toggleAllLabel?: string;
title: ReactNode;
variant?: "default" | "inline" | "collapse";
}
export const CheckboxGroup: FC<CheckboxGroupProps> = ({
accordionSummary = "Options",
accordionDefaultOpen,
checkboxes,
className,
disabled,
description,
name,
onToggleAll,
showAccordion,
themed,
title,
toggleAllLabel = "Enable All Options",
variant = "default",
...props
}) => {
const isInlineVariant = variant === "inline";
const isCollapseVariant = variant === "collapse";
const sortedCheckboxes = orderBy(checkboxes, "sortOrder");
const allChecksEnabled = sortedCheckboxes.every(
(checkbox) => checkbox.initiallyEnabled
);
// Determine whether to render a div or an Accordion
const ContentWrapper = showAccordion ? Accordion : "div";
// Set props specific to Accordion if needed
const accordionProps = showAccordion
? {
summary: accordionSummary,
forceShow: accordionDefaultOpen,
variant: "text",
}
: {};
const checkboxList = sortedCheckboxes.map((checkbox, i) => {
const id = `${name}-${i}`;
return (
<div
key={i}
className={styles.checkboxOption}
data-testid="form-checkbox-group"
>
<Checkbox
className={styles.checkbox}
id={id}
name={name}
checked={checkbox.initiallyEnabled}
disabled={disabled || checkbox.disabled}
themed={themed}
onChange={checkbox.onChange}
onChangeConfirm={checkbox.onChangeConfirm}
value={checkbox.value}
/>
<div className={styles.optionContent}>
<label className={styles.label} htmlFor={id}>
{checkbox.label}
</label>
{checkbox.description && (
<div
className={styles.description}
dangerouslySetInnerHTML={{ __html: checkbox.description }}
/>
)}
</div>
</div>
);
});
/** Handle Collapse Variant separately since it's structure is different */
if (isCollapseVariant)
return (
<Accordion
className={clsx([
styles.checkboxGroup,
styles.collapse,
themed && styles.themed,
className,
])}
summary={<p className={styles.checkboxGroupTitle}>{title}</p>}
variant="text"
forceShow={accordionDefaultOpen}
{...props}
>
{description && (
<div className={styles.checkboxGroupDescription}>{description}</div>
)}
{onToggleAll && (
<Checkbox
className={clsx([styles.checkbox, styles.toggleAll])}
id={`${name}-toggle-all`}
checked={allChecksEnabled}
aria-label="Check/Uncheck all"
onChange={onToggleAll}
themed={themed}
label={toggleAllLabel}
/>
)}
{checkboxList}
</Accordion>
);
return (
<div
className={clsx([
styles.checkboxGroup,
styles[variant],
themed && styles.themed,
className,
])}
{...props}
>
<header>
<div className={styles.checkboxGroupHeader}>
<p className={styles.checkboxGroupTitle}>{title}</p>
{isInlineVariant && checkboxes.length < 2 && (
<Checkbox
className={styles.checkbox}
id={`${name}-0`}
name={name}
checked={checkboxes[0].initiallyEnabled}
disabled={disabled || checkboxes[0].disabled}
themed={themed}
onChange={checkboxes[0].onChange}
onChangeConfirm={checkboxes[0].onChangeConfirm}
value={checkboxes[0].value}
/>
)}
</div>
{description && (
<div className={styles.checkboxGroupDescription}>{description}</div>
)}
</header>
{!isInlineVariant && onToggleAll && (
<Checkbox
className={clsx([styles.checkbox, styles.toggleAll])}
id={`${name}-toggle-all`}
checked={allChecksEnabled}
aria-label="Check/Uncheck all"
onChange={onToggleAll}
themed={themed}
label={toggleAllLabel}
/>
)}
{!isInlineVariant && (
<ContentWrapper
className={styles.checkboxOptions}
{...(accordionProps as AccordionProps)}
>
{checkboxList}
</ContentWrapper>
)}
</div>
);
};
@@ -2,7 +2,8 @@ import clsx from "clsx";
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/solid/x.svg"; import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/solid/x.svg";
import { Dialog, DialogProps } from "@dndbeyond/ttui/components/Dialog";
import { Dialog, DialogProps } from "~/components/Dialog";
import { Button, ButtonProps } from "../Button"; import { Button, ButtonProps } from "../Button";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
+21
View File
@@ -0,0 +1,21 @@
import clsx from "clsx";
import { FC } from "react";
import {
Dialog as TtuiDialog,
DialogProps as TtuiDialogProps,
} from "@dndbeyond/ttui/components/Dialog";
import styles from "./styles.module.css";
/**
* A wrapper around the TTUI Dialog component to apply custom styles. This is necessary because the
* transform property conflicts with styles for the select component. Using this component to
* remove those styles resolves the issue.
*/
export interface DialogProps extends TtuiDialogProps {}
export const Dialog: FC<DialogProps> = ({ className, ...props }) => {
return <TtuiDialog className={clsx(styles.dialog, className)} {...props} />;
};
@@ -1,12 +1,13 @@
import { FC, HTMLAttributes } from "react"; import { FC, HTMLAttributes } from "react";
import { useDispatch } from "react-redux";
import { import {
characterActions, characterActions,
ChoiceUtils, ChoiceUtils,
ContainerManager,
} from "@dndbeyond/character-rules-engine"; } from "@dndbeyond/character-rules-engine";
import { useCharacterEngine } from "~/hooks/useCharacterEngine"; import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useDispatch } from "~/hooks/useDispatch";
import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice"; import { DetailChoice } from "~/tools/js/Shared/containers/DetailChoice";
import { CharClass, Choice, ClassDefinitionContract, Feat } from "~/types"; import { CharClass, Choice, ClassDefinitionContract, Feat } from "~/types";
@@ -15,6 +16,7 @@ export interface FeatureChoiceProps extends HTMLAttributes<HTMLDivElement> {
charClass?: CharClass; charClass?: CharClass;
featsData: Feat[]; featsData: Feat[];
subclassData?: ClassDefinitionContract[]; subclassData?: ClassDefinitionContract[];
itemShoppe?: ContainerManager | null;
onChoiceChange: ( onChoiceChange: (
choiceId: string, choiceId: string,
type: number, type: number,
@@ -33,6 +35,7 @@ export const FeatureChoice: FC<FeatureChoiceProps> = ({
choice, choice,
featsData, featsData,
subclassData, subclassData,
itemShoppe,
onChoiceChange, onChoiceChange,
className, className,
collapseDescription, collapseDescription,
@@ -69,13 +72,14 @@ export const FeatureChoice: FC<FeatureChoiceProps> = ({
} }
}; };
const { description, options, featSubChoices } = const { description, options, featSubChoices, introDescription } =
ChoiceUtils.getSortedChoiceOptionsForFeaturesInfo( ChoiceUtils.getSortedChoiceOptionsForFeaturesInfo(
choice, choice,
featsData, featsData,
featLookup, featLookup,
charClass || null, charClass || null,
subclassData || [], subclassData || [],
itemShoppe || null,
preferences, preferences,
prerequisiteData, prerequisiteData,
ruleData, ruleData,
@@ -90,6 +94,7 @@ export const FeatureChoice: FC<FeatureChoiceProps> = ({
options={options} options={options}
onChange={handleChoiceChange} onChange={handleChoiceChange}
description={description || ""} description={description || ""}
descriptionIntro={introDescription || ""}
choiceInfo={choiceInfo} choiceInfo={choiceInfo}
classId={charClass && classUtils.getId(charClass)} classId={charClass && classUtils.getId(charClass)}
showBackgroundProficiencyOptions={true} showBackgroundProficiencyOptions={true}
@@ -1,5 +1,5 @@
import clsx from "clsx"; import clsx from "clsx";
import { FC, HTMLAttributes, useEffect, useState } from "react"; import { FC, HTMLAttributes, useContext, useEffect, useState } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { import {
@@ -9,13 +9,17 @@ import {
Choice, Choice,
ChoiceUtils, ChoiceUtils,
ClassDefinitionContract, ClassDefinitionContract,
ClassFeature,
ClassFeatureUtils,
ClassUtils, ClassUtils,
Constants, Constants,
ContainerManager,
Feat, Feat,
FeatUtils, FeatUtils,
} from "@dndbeyond/character-rules-engine"; } from "@dndbeyond/character-rules-engine";
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading"; import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
import { InventoryManagerContext } from "~/tools/js/Shared/managers/InventoryManagerContext";
import { appEnvSelectors } from "~/tools/js/Shared/selectors"; import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import { AppLoggerUtils } from "~/tools/js/Shared/utils"; import { AppLoggerUtils } from "~/tools/js/Shared/utils";
import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder"; import LoadingPlaceholder from "~/tools/js/smartComponents/LoadingPlaceholder";
@@ -35,7 +39,9 @@ export interface FeatureChoicesProps extends HTMLAttributes<HTMLDivElement> {
) => void; ) => void;
collapseDescription?: boolean; collapseDescription?: boolean;
charClass?: CharClass; charClass?: CharClass;
classFeature?: ClassFeature;
shouldFetch?: boolean; // Optional prop to control fetching behavior shouldFetch?: boolean; // Optional prop to control fetching behavior
itemShoppe?: ContainerManager | null; // Optional prop for cases where the itemShoppe is fetched from the parent component (only used if the Feature Choices component is rendered multiple times in the same parent - e.g. in the Item Plans Manage Pane)
} }
export const FeatureChoices: FC<FeatureChoicesProps> = ({ export const FeatureChoices: FC<FeatureChoicesProps> = ({
@@ -45,7 +51,9 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
onChoiceChange, onChoiceChange,
collapseDescription, collapseDescription,
charClass, charClass,
classFeature,
shouldFetch = true, // Default to true if not provided shouldFetch = true, // Default to true if not provided
itemShoppe = null,
...props ...props
}) => { }) => {
const isReadonly = useSelector(appEnvSelectors.getIsReadonly); const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
@@ -56,14 +64,20 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
apiCreatorSelectors.makeLoadAvailableSubclasses apiCreatorSelectors.makeLoadAvailableSubclasses
); );
const { inventoryManager } = useContext(InventoryManagerContext);
const [subclassData, setSubclassData] = useState< const [subclassData, setSubclassData] = useState<
Array<ClassDefinitionContract> Array<ClassDefinitionContract>
>([]); >([]);
const [featsData, setFeatsData] = useState<Array<Feat>>([]); const [featsData, setFeatsData] = useState<Array<Feat>>([]);
const [localItemShoppe, setLocalItemShoppe] =
useState<ContainerManager | null>(itemShoppe);
const [featLoadingStatus, setFeatLoadingStatus] = const [featLoadingStatus, setFeatLoadingStatus] =
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED); useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
const [subclassLoadingStatus, setSubclassLoadingStatus] = const [subclassLoadingStatus, setSubclassLoadingStatus] =
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED); useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
const [itemsLoadingStatus, setItemsLoadingStatus] =
useState<DataLoadingStatusEnum>(DataLoadingStatusEnum.NOT_INITIALIZED);
const hasFeatChoice = choices.some( const hasFeatChoice = choices.some(
(choice) => (choice) =>
@@ -77,15 +91,23 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
Constants.BuilderChoiceTypeEnum.SUB_CLASS_OPTION Constants.BuilderChoiceTypeEnum.SUB_CLASS_OPTION
); );
const hasItemMappings = classFeature
? ClassFeatureUtils.getHasItemMappings(classFeature)
: false;
useEffect(() => { useEffect(() => {
const abortController = new AbortController();
if ( if (
hasFeatChoice && hasFeatChoice &&
shouldFetch && shouldFetch &&
featLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED featLoadingStatus !== DataLoadingStatusEnum.LOADED
) { ) {
setFeatLoadingStatus(DataLoadingStatusEnum.LOADING); setFeatLoadingStatus(DataLoadingStatusEnum.LOADING);
loadAvailableFeats() loadAvailableFeats({
signal: abortController.signal,
})
.then((response) => { .then((response) => {
let featsData: Array<Feat> = []; let featsData: Array<Feat> = [];
@@ -99,17 +121,22 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
setFeatsData(featsData); setFeatsData(featsData);
setFeatLoadingStatus(DataLoadingStatusEnum.LOADED); setFeatLoadingStatus(DataLoadingStatusEnum.LOADED);
}) })
.catch(AppLoggerUtils.handleAdhocApiError); .catch((e) => {
if (abortController.signal.aborted) return;
AppLoggerUtils.handleAdhocApiError(e);
});
} }
if ( if (
charClass && charClass &&
hasSubclassChoice && hasSubclassChoice &&
shouldFetch && shouldFetch &&
subclassLoadingStatus === DataLoadingStatusEnum.NOT_INITIALIZED subclassLoadingStatus !== DataLoadingStatusEnum.LOADED
) { ) {
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADING); setSubclassLoadingStatus(DataLoadingStatusEnum.LOADING);
loadAvailableSubclasses(ClassUtils.getId(charClass)) loadAvailableSubclasses(ClassUtils.getId(charClass), {
signal: abortController.signal,
})
.then((response) => { .then((response) => {
let subclassData: Array<ClassDefinitionContract> = []; let subclassData: Array<ClassDefinitionContract> = [];
@@ -121,17 +148,55 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
setSubclassData(subclassData); setSubclassData(subclassData);
setSubclassLoadingStatus(DataLoadingStatusEnum.LOADED); setSubclassLoadingStatus(DataLoadingStatusEnum.LOADED);
}) })
.catch(AppLoggerUtils.handleAdhocApiError); .catch((e) => {
if (abortController.signal.aborted) return;
AppLoggerUtils.handleAdhocApiError(e);
});
} }
if (
localItemShoppe === null &&
hasItemMappings &&
shouldFetch &&
itemsLoadingStatus !== DataLoadingStatusEnum.LOADED
) {
if (inventoryManager) {
setItemsLoadingStatus(DataLoadingStatusEnum.LOADING);
inventoryManager
.getInventoryShoppe({
onSuccess: (shoppeContainer: ContainerManager) => {
setLocalItemShoppe(shoppeContainer);
setItemsLoadingStatus(DataLoadingStatusEnum.LOADED);
},
additionalApiConfig: {
signal: abortController.signal,
},
})
.catch((e) => {
if (abortController.signal.aborted) return;
AppLoggerUtils.handleAdhocApiError(e);
});
} else {
setItemsLoadingStatus(DataLoadingStatusEnum.LOADED);
}
}
return () => {
abortController.abort();
};
}, [ }, [
hasFeatChoice, hasFeatChoice,
hasSubclassChoice, hasSubclassChoice,
hasItemMappings,
subclassLoadingStatus, subclassLoadingStatus,
featLoadingStatus, featLoadingStatus,
itemsLoadingStatus,
charClass, charClass,
loadAvailableFeats, loadAvailableFeats,
loadAvailableSubclasses, loadAvailableSubclasses,
shouldFetch, shouldFetch,
inventoryManager,
]); ]);
const isDataLoaded = (): boolean => { const isDataLoaded = (): boolean => {
@@ -145,6 +210,13 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
return false; return false;
} }
} }
if (itemShoppe !== null) {
return true;
} else if (hasItemMappings) {
if (itemsLoadingStatus !== DataLoadingStatusEnum.LOADED) {
return false;
}
}
return true; return true;
}; };
@@ -163,6 +235,7 @@ export const FeatureChoices: FC<FeatureChoicesProps> = ({
charClass={charClass} charClass={charClass}
choice={choice} choice={choice}
featsData={featsData} featsData={featsData}
itemShoppe={localItemShoppe}
subclassData={subclassData} subclassData={subclassData}
className={styles.choice} className={styles.choice}
key={ChoiceUtils.getId(choice)} key={ChoiceUtils.getId(choice)}
@@ -0,0 +1,98 @@
import { StepType, TourContext, TourProvider } from "@reactour/tour";
import clsx from "clsx";
import { Dispatch, FC, HTMLAttributes, SetStateAction } from "react";
import QuestionIcon from "@dndbeyond/fontawesome-cache/svgs/solid/question.svg";
import { Button } from "../Button";
import styles from "./styles.module.css";
export interface GuidedTourProps extends HTMLAttributes<HTMLDivElement> {
afterOpen?: (target: Element | null) => void;
beforeClose?: (target: Element | null) => void;
cookieName?: string;
setStep?: SetStateAction<any>;
showOnFirstLoad?: boolean;
steps?: StepType[];
step?: number;
}
export const GuidedTour: FC<GuidedTourProps> = ({
afterOpen,
beforeClose,
children,
className,
cookieName = "ddbGuidedTour",
setStep,
showOnFirstLoad,
step = 0,
steps = [],
...props
}) => {
const handleBeforeClose = () => {
const html = document.querySelector("html");
if (html) html.style.overflowY = "visible";
if (beforeClose) beforeClose(null);
};
const handleAfterOpen = () => {
const html = document.querySelector("html");
if (html) html.style.overflowY = "hidden";
if (afterOpen) afterOpen(null);
};
const handleLoad = (setIsOpen: Dispatch<SetStateAction<boolean>>) => {
if (showOnFirstLoad) {
const viewedTour = localStorage.getItem(cookieName);
if (!viewedTour) {
setIsOpen(true);
localStorage.setItem(cookieName, "true");
}
}
};
const PrevButton = ({ Button, ...props }) => (
<Button {...props} kind="prev" hideArrow={step === 0} />
);
const NextButton = ({ Button, ...props }) => (
<Button
{...props}
kind="next"
hideArrow={step === (steps?.length || 0) - 1}
/>
);
return (
<div className={clsx([styles.guidedTour, className])} {...props}>
<TourProvider
steps={steps}
currentStep={step}
setCurrentStep={setStep}
afterOpen={handleAfterOpen}
beforeClose={handleBeforeClose}
showDots={false}
prevButton={PrevButton}
nextButton={NextButton}
>
<TourContext.Consumer>
{({ setIsOpen }) => (
<div onLoad={() => handleLoad(setIsOpen)}>
{children}
<Button
className={styles.tourButton}
onClick={() => setIsOpen(true)}
color="info"
size="small"
aria-label="Open guided tour"
>
<QuestionIcon />
</Button>
</div>
)}
</TourContext.Consumer>
</TourProvider>
</div>
);
};
@@ -0,0 +1,41 @@
import { useTour } from "@reactour/tour";
import clsx from "clsx";
import { FC, HTMLAttributes, ReactNode } from "react";
import { Button } from "../Button";
import styles from "./styles.module.css";
export interface GuidedTourStepProps
extends Omit<HTMLAttributes<HTMLDivElement>, "content"> {
title: string;
content: ReactNode;
showClose?: boolean;
}
export const GuidedTourStep: FC<GuidedTourStepProps> = ({
className,
content,
showClose,
title,
...props
}) => {
const { setIsOpen } = useTour();
return (
<div className={clsx([styles.guidedTourStep, className])} {...props}>
<p className={styles.title}>{title}</p>
<div className={styles.content}>{content}</div>
{showClose && (
<Button
className={styles.closeButton}
size="small"
variant="outline"
themed
onClick={() => setIsOpen(false)}
>
Start Playing!
</Button>
)}
</div>
);
};
@@ -1,3 +1,4 @@
import clsx from "clsx";
import { FC } from "react"; import { FC } from "react";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
@@ -16,12 +17,14 @@ export interface HelperTextAccordionProps
export const HelperTextAccordion: FC<HelperTextAccordionProps> = ({ export const HelperTextAccordion: FC<HelperTextAccordionProps> = ({
builderHelperText, builderHelperText,
className,
...props ...props
}) => { }) => {
return ( return (
<> <>
{builderHelperText.map((helperText, idx) => ( {builderHelperText.map((helperText, idx) => (
<Accordion <Accordion
className={clsx([styles.accordion, className])}
{...props} {...props}
key={uuidv4()} key={uuidv4()}
summary={ summary={
+17 -22
View File
@@ -1,35 +1,30 @@
import clsx from "clsx"; import clsx from "clsx";
import { FC, HTMLAttributes } from "react"; import type { FC, HTMLAttributes } from "react";
import { InfoItem as TtuiInfoItem } from "@dndbeyond/ttui/components/InfoItem";
import { useUnpropagatedClick } from "~/hooks/useUnpropagatedClick";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
interface InfoItemProps extends HTMLAttributes<HTMLElement> { interface InfoItemProps extends HTMLAttributes<HTMLDivElement> {
label: string; label: string;
color?: "primary"; // Not needed here, but fixes ts error
inline?: boolean; inline?: boolean;
} }
/** /**
* This component is an attribute for a given item, spell, or other entity. * This component is an attribute for a given item, spell, or other entity.
* The InfoItemList component is a customized version of the InfoItem component from */
* the @dndbeyond/ttui library.
**/
export const InfoItem: FC<InfoItemProps> = ({ export const InfoItem: FC<InfoItemProps> = ({
className, className,
onClick, inline = true,
label,
children,
...props ...props
}) => { }) => (
const handleClick = useUnpropagatedClick(onClick); <div
className={clsx([styles.item, inline && styles.inline, className])}
return ( role="listitem"
<TtuiInfoItem {...props}
className={clsx([styles.item, className])} >
onClick={onClick ? handleClick : undefined} <p className={styles.label}>{label}</p>
{...props} <p className={styles.value}>{children}</p>
/> </div>
); );
};
@@ -0,0 +1,107 @@
import clsx from "clsx";
import { FC, HTMLAttributes, MouseEvent } from "react";
import {
AccessUtils,
CampaignUtils,
FormatUtils,
Infusion,
InfusionUtils,
RuleDataUtils,
Constants,
} from "@dndbeyond/character-rules-engine/es";
import { MarketplaceCta } from "~/components/MarketplaceCta";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import Snippet from "../../tools/js/smartComponents/Snippet";
import styles from "./styles.module.css";
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
infusion: Infusion;
onClick?: (infusion: Infusion) => void;
}
export const InfusionPreview: FC<Props> = ({
infusion,
onClick,
className,
...props
}) => {
const { ruleData, snippetData, proficiencyBonus, partyInfo } =
useCharacterEngine();
const itemMappingId = InfusionUtils.getInventoryMappingId(infusion);
const partyRestrictions = partyInfo
? CampaignUtils.getPartyInventoryRestrictions(partyInfo)
: [];
const isInfusedItemInParty = partyRestrictions.some(
(restriction) =>
restriction.type === Constants.PartyRestrictionTypeEnum.INFUSION &&
restriction.inventoryMappingId === itemMappingId
);
const handleClick = (evt: MouseEvent): void => {
if (onClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onClick(infusion);
}
};
//if you have access to the infusion via entitlments or the infused item is in the party - you can see the details, othwise show marketplace cta
const isAccessible =
AccessUtils.isAccessible(InfusionUtils.getAccessType(infusion)) ||
isInfusedItemInParty;
const getSourceNames = (): string | null => {
const sources = InfusionUtils.getSources(infusion);
let sourceNames: Array<string> = [];
if (sources.length > 0) {
sources.forEach((source) => {
let sourceInfo = RuleDataUtils.getSourceDataInfo(
source.sourceId,
ruleData
);
if (sourceInfo !== null && sourceInfo.description !== null) {
sourceNames.push(sourceInfo.description);
}
});
}
return sourceNames.length > 0
? FormatUtils.renderNonOxfordCommaList(sourceNames)
: null;
};
return (
<div
className={clsx([onClick && styles.isInteractive, className])}
onClick={handleClick}
{...props}
>
<div className={styles.name}>
Infusion: {InfusionUtils.getName(infusion)}
</div>
<div className={styles.snippet}>
{isAccessible ? (
<Snippet
snippetData={snippetData}
classLevel={
InfusionUtils.getSelectedModifierData(infusion)?.value ?? null
}
proficiencyBonus={proficiencyBonus}
>
{InfusionUtils.getSnippet(infusion)}
</Snippet>
) : (
<MarketplaceCta
sourceName={getSourceNames()}
description="To unlock this infusion, check out the Marketplace to view purchase options."
/>
)}
</div>
</div>
);
};
+58 -14
View File
@@ -1,10 +1,10 @@
import clsx from "clsx"; import clsx from "clsx";
import { FC, HTMLAttributes } from "react"; import { FC, HTMLAttributes } from "react";
import { useMatch } from "react-router-dom"; import { createPortal } from "react-dom";
import { useMatch, useSearchParams } from "react-router-dom";
import { NavigationMenu } from "@dndbeyond/navigation-menu";
import { Footer } from "@dndbeyond/ttui/components/Footer"; import { Footer } from "@dndbeyond/ttui/components/Footer";
import { MegaMenu } from "@dndbeyond/ttui/components/MegaMenu";
import { Sitebar } from "@dndbeyond/ttui/components/Sitebar";
import config from "~/config"; import config from "~/config";
import useUser from "~/hooks/useUser"; import useUser from "~/hooks/useUser";
@@ -13,9 +13,40 @@ import styles from "./styles.module.css";
const BASE_PATHNAME = config.basePathname; const BASE_PATHNAME = config.basePathname;
const NOTIFICATIONS_CACHE_KEY = "dndbNotificationsCache";
const seedNotificationsCache = () => {
try {
const existing = localStorage.getItem(NOTIFICATIONS_CACHE_KEY);
const parsed = existing ? JSON.parse(existing) : null;
const isValid =
parsed &&
typeof parsed.expiresAtMs === "number" &&
Date.now() < parsed.expiresAtMs;
if (!isValid) {
localStorage.setItem(
NOTIFICATIONS_CACHE_KEY,
JSON.stringify({
expiresAtMs: Date.now() + 60 * 60 * 1000,
counts: {
unreadNotifications: null,
unreadMessages: null,
unreadReports: null,
unreadAppeals: null,
},
})
);
}
} catch {
// localStorage unavailable
}
};
interface LayoutProps extends HTMLAttributes<HTMLDivElement> {} interface LayoutProps extends HTMLAttributes<HTMLDivElement> {}
export const Layout: FC<LayoutProps> = ({ children, ...props }) => { export const Layout: FC<LayoutProps> = ({ children, ...props }) => {
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
const isDev = process.env.NODE_ENV === "development"; const isDev = process.env.NODE_ENV === "development";
const user = useUser(); const user = useUser();
const matchSheet = useMatch(`${BASE_PATHNAME}/:characterId/`); const matchSheet = useMatch(`${BASE_PATHNAME}/:characterId/`);
@@ -23,20 +54,33 @@ export const Layout: FC<LayoutProps> = ({ children, ...props }) => {
// Don't show the navigation in production // Don't show the navigation in production
if (!isDev) return <>{children}</>; if (!isDev) return <>{children}</>;
// Seed the notifications cache so NavigationMenu skips the JWT fetch in local
// dev, where the auth service is not proxied.
seedNotificationsCache();
const navigationUser =
user && user.avatarUrl
? {
displayName: user.displayName,
avatarUrl: user.avatarUrl,
subscriptionTier: user.subscriptionTier as "Master",
roles: user.roles,
}
: undefined;
return ( return (
<div {...props}> <div {...props}>
{/* TODO: fetch navItems */} {!isVttView &&
<div className={styles.siteStyles}> createPortal(
<Sitebar user={user as any} navItems={[]} /> <NavigationMenu user={navigationUser} />,
{/* TODO: fetch sources */} document.getElementById("header-wrapper")!
<MegaMenu enablePartyWizard={true} sources={[]} /> )}
</div>
<div className={clsx(["container", styles.devContainer])}>{children}</div> <div className={clsx(["container", styles.devContainer])}>{children}</div>
{!matchSheet && ( {!isVttView &&
<div className={styles.siteStyles}> (!matchSheet ||
<Footer /> matchSheet.pathname === "/characters/builder" ||
</div> matchSheet.pathname === "/characters/premade") &&
)} createPortal(<Footer />, document.getElementById("footer-wrapper")!)}
</div> </div>
); );
}; };
+3 -3
View File
@@ -6,7 +6,7 @@ interface LinkProps
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "onClick"> { extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "onClick"> {
onClick?: Function; onClick?: Function;
useTheme?: boolean; useTheme?: boolean;
useRouter?: boolean; userouter?: boolean;
} }
export const Link: FC<LinkProps> = ({ export const Link: FC<LinkProps> = ({
@@ -15,7 +15,7 @@ export const Link: FC<LinkProps> = ({
href, href,
onClick, onClick,
useTheme, useTheme,
useRouter, userouter,
...props ...props
}) => { }) => {
//TODO - refactor to handle stop propagation on the oustide of this component (about 7 or so files to change) //TODO - refactor to handle stop propagation on the oustide of this component (about 7 or so files to change)
@@ -27,7 +27,7 @@ export const Link: FC<LinkProps> = ({
} }
}; };
if (useRouter) if (userouter)
return ( return (
<RouterLink <RouterLink
className={className} className={className}
@@ -0,0 +1,53 @@
import { clsx } from "clsx";
import { FC, HTMLAttributes } from "react";
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
import styles from "./styles.module.css";
interface Props extends HTMLAttributes<HTMLDivElement> {
sourceName?: string | null;
sourceNames?: Array<string>;
url?: string;
description?: string | null;
className?: string;
}
export const MarketplaceCta: FC<Props> = ({
sourceName = null,
sourceNames,
url = "/marketplace",
description = null,
className,
...props
}) => {
const allSourceNames: Array<string> = [];
if (sourceName) {
allSourceNames.push(sourceName);
}
if (sourceNames) {
allSourceNames.push(...sourceNames);
}
return (
<div className={clsx([styles.cta, className])} {...props}>
<a href={url} target="_blank" className={styles.link} rel="noreferrer">
<div className={styles.content}>
{allSourceNames.length > 0 && (
<div className={styles.header}>
<div className={styles.headerIntro}>Available in</div>
<div className={styles.headerTitle}>
{FormatUtils.renderNonOxfordCommaList(allSourceNames)}
</div>
</div>
)}
{description && (
<div className={styles.description}>{description}</div>
)}
<div className={styles.button}>Go To Marketplace</div>
</div>
</a>
</div>
);
};
@@ -1,65 +1,74 @@
import { HTMLAttributes, useEffect, useRef } from "react"; import clsx from "clsx";
import { Button } from "@dndbeyond/ttui/components/Button"; import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/solid/x.svg";
import { Button } from "../Button";
import { Dialog, DialogProps } from "../Dialog";
import { MaxCharactersMessageText } from "../MaxCharactersMessageText"; import { MaxCharactersMessageText } from "../MaxCharactersMessageText";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
interface MaxCharactersDialogProps extends HTMLAttributes<HTMLDialogElement> { interface MaxCharactersDialogProps
extends Omit<DialogProps, "onClose" | "open"> {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
useMyCharactersLink?: boolean; useMyCharactersLink?: boolean;
hasLockedCharacters?: boolean;
} }
export const MaxCharactersDialog: React.FC<MaxCharactersDialogProps> = ({ export const MaxCharactersDialog: React.FC<MaxCharactersDialogProps> = ({
open, open,
onClose, onClose,
useMyCharactersLink, useMyCharactersLink,
hasLockedCharacters,
className,
...props ...props
}) => { }) => {
const dialogRef = useRef<HTMLDialogElement>(null);
const handleClickBackdrop = (e) => {
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) (dialogRef.current as any)?.close();
};
useEffect(() => {
if (open) {
// Hacky workaround since showModal() isn't
// supported by TypeScript <4.8.3
(dialogRef.current as any)?.showModal();
} else {
// Hacky workaround since close() isn't
// supported by TypeScript <4.8.3
(dialogRef.current as any)?.close();
}
}, [onClose, open]);
return ( return (
<dialog <Dialog
className={styles.maxCharactersDialog} className={clsx([styles.dialog, className])}
ref={dialogRef} open={open}
onClick={handleClickBackdrop} onClose={onClose}
modal
{...props} {...props}
> >
<header> <header className={styles.header}>
<h2 className={styles.dialogTitle}>Your character slots are full</h2> <h2 className={styles.title}>Your character slots are full</h2>
</header> <Button
<MaxCharactersMessageText useMyCharactersLink={useMyCharactersLink} /> className={styles.closeButton}
<footer className={styles.dialogFooter}> variant="tool"
<Button href="/store/subscribe" variant="outline"> onClick={onClose}
Subscribe aria-label="Close Modal"
>
<CloseIcon />
</Button>
</header>
<div className={styles.content}>
<MaxCharactersMessageText
hasLockedCharacters={hasLockedCharacters}
useMyCharactersLink={useMyCharactersLink}
/>
</div>
<footer className={styles.footer}>
{!hasLockedCharacters && (
<Button
className={styles.cardButton}
href="/store/subscribe"
color="info"
size="x-small"
variant="outline"
>
Subscribe
</Button>
)}
<Button
className={styles.cardButton}
color="info"
size="x-small"
onClick={onClose}
>
Close
</Button> </Button>
<Button onClick={onClose}>Close</Button>
</footer> </footer>
</dialog> </Dialog>
); );
}; };
@@ -6,12 +6,14 @@ interface MaxCharactersMessageTextProps extends HTMLAttributes<HTMLDivElement> {
includeTitle?: boolean; includeTitle?: boolean;
useLinks?: boolean; useLinks?: boolean;
useMyCharactersLink?: boolean; useMyCharactersLink?: boolean;
hasLockedCharacters?: boolean;
} }
export const MaxCharactersMessageText: FC<MaxCharactersMessageTextProps> = ({ export const MaxCharactersMessageText: FC<MaxCharactersMessageTextProps> = ({
includeTitle, includeTitle,
useLinks, useLinks,
useMyCharactersLink, useMyCharactersLink,
hasLockedCharacters,
...props ...props
}) => { }) => {
const subscribe = useLinks ? ( const subscribe = useLinks ? (
@@ -31,6 +33,21 @@ export const MaxCharactersMessageText: FC<MaxCharactersMessageTextProps> = ({
"My Characters" "My Characters"
); );
if (hasLockedCharacters) {
return (
<div {...props}>
<p className={styles.messageSubtext}>
Your character slots are full. To create a new character, select which
characters to keep using the options below, or{" "}
<a className={styles.link} href="/store/subscribe">
subscribe
</a>{" "}
to unlock additional slots.
</p>
</div>
);
}
return ( return (
<div {...props}> <div {...props}>
{includeTitle && ( {includeTitle && (
@@ -0,0 +1,18 @@
import { HTMLAttributes, FC, ReactNode } from "react";
import styles from "./styles.module.css";
export interface NoResultsFoundProps extends HTMLAttributes<HTMLDivElement> {
message?: ReactNode;
}
export const NoResultsFound: FC<NoResultsFoundProps> = ({
message = "No results found",
...props
}) => {
return (
<p className={styles.noResults} {...props}>
{message}
</p>
);
};
+17 -12
View File
@@ -7,21 +7,23 @@ import React, {
useEffect, useEffect,
} from "react"; } from "react";
import { Dialog } from "@dndbeyond/ttui/components/Dialog";
import { useIsVisible } from "@dndbeyond/ttui/hooks/useIsVisible"; import { useIsVisible } from "@dndbeyond/ttui/hooks/useIsVisible";
import { Dialog } from "~/components/Dialog";
import { useCharacterTheme } from "~/contexts/CharacterTheme"; import { useCharacterTheme } from "~/contexts/CharacterTheme";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
/** /**
* This is a popover component to handle dialogs that are dependent on a location from a trigger element. * This is a popover component to handle dialogs that are dependent on a location from a trigger
* It uses the useIsVisible hook to handle the visibility of the dialog. The position of the dialog * element. It uses the useIsVisible hook to handle the visibility of the dialog. The position of
* is determined by the position prop, which can be set to topLeft, topRight, left, right, bottomLeft, or bottomRight. * the dialog is determined by the position prop, which can be set to topLeft, topRight, left,
* The maxWidth prop can be used to set the maximum width of the dialog. The trigger prop is the element that will trigger * right, bottomLeft, or bottomRight. The maxWidth prop can be used to set the maximum width of the
* the dialog to open.The popover component also handles the positioning of the dialog based on the trigger element. The * dialog. The trigger prop is the element that will trigger the dialog to open.The popover
* handleToggle function toggles the visibility of the dialog, and the handleClose function closes the dialog. The popover * component also handles the positioning of the dialog based on the trigger element. The
* component also adds an event listener to the cancel button in the dialog to close the dialog when clicked. * handleToggle function toggles the visibility of the dialog, and the handleClose function closes
* the dialog. The popover component also adds an event listener to the cancel button in the dialog
* to close the dialog when clicked.
* *
* In order for a cancel button to work, it must have a `data-cancel` attribute. * In order for a cancel button to work, it must have a `data-cancel` attribute.
**/ **/
@@ -36,7 +38,7 @@ interface PopoverProps {
| "bottomLeft" | "bottomLeft"
| "bottomRight" | "bottomRight"
| "bottom"; | "bottom";
trigger: ReactElement; trigger: ReactElement<{ onClick?: (e: MouseEvent) => void }>;
maxWidth?: number; maxWidth?: number;
variant?: "default" | "menu"; variant?: "default" | "menu";
"data-testid"?: string; "data-testid"?: string;
@@ -85,9 +87,12 @@ export const Popover: FC<PopoverProps> = ({
// Check if children is a ReactElement // Check if children is a ReactElement
if (React.isValidElement(children)) { if (React.isValidElement(children)) {
// Create a copy of the ReactElement and attach handleClose function to it // Create a copy of the ReactElement and attach handleClose function to it
return cloneElement(children as ReactElement, { return cloneElement(
handleClose, children as ReactElement<{ handleClose: (e: MouseEvent) => void }>,
}); {
handleClose,
}
);
} }
// If not just return the children - means its a primitive type // If not just return the children - means its a primitive type
else { else {
@@ -0,0 +1,34 @@
import { FC } from "react";
import { PreferencePrivacyTypeEnum } from "~/constants";
import { RadioGroup, RadioGroupProps } from "../RadioGroup";
export interface PrivacyTypeRadioProps
extends Omit<RadioGroupProps, "name" | "title" | "options"> {}
export const PrivacyTypeRadio: FC<PrivacyTypeRadioProps> = (props) => (
<RadioGroup
title="Character Privacy"
name="privacyType"
options={[
{
value: PreferencePrivacyTypeEnum.CAMPAIGN_ONLY,
label: "Campaign Only",
description:
"Only others within a Campaign you have joined can view your Characters.",
},
{
value: PreferencePrivacyTypeEnum.PUBLIC,
label: "Public",
description: "Anyone with a shared link can view your Characters.",
},
{
value: PreferencePrivacyTypeEnum.PRIVATE,
label: "Private",
description: "Only you can view your Characters.",
},
]}
{...props}
/>
);
@@ -0,0 +1,117 @@
import clsx from "clsx";
import { ChangeEvent, FC, HTMLAttributes, useRef, useState } from "react";
import styles from "./styles.module.css";
export interface RadioOption {
label: string;
value: string | number;
description?: string;
disabled?: boolean;
}
export interface RadioGroupProps
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
name: string;
initialValue?: string | number;
title: string;
options: RadioOption[];
description?: string;
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
onChangeConfirm?: (
e: ChangeEvent<HTMLInputElement>,
newValue: string | number,
oldValue: string | number,
accept: () => void,
reject: () => void
) => void;
disabled?: boolean;
themed?: boolean;
}
export const RadioGroup: FC<RadioGroupProps> = ({
className,
name,
initialValue,
title,
options,
description,
disabled,
onChange,
onChangeConfirm,
themed,
...props
}) => {
const radioGroupRef = useRef<HTMLDivElement>(null!);
const [selectedValue, setSelectedValue] = useState(
initialValue || options[0]?.value
);
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const newValue =
typeof selectedValue === "number"
? Number(e.target.value)
: e.target.value;
if (onChangeConfirm) {
onChangeConfirm(
e,
newValue,
selectedValue,
() => {
if (onChange) onChange(e);
setSelectedValue(newValue);
},
() => {
// noop
}
);
} else {
if (onChange) onChange(e);
setSelectedValue(newValue);
}
};
return (
<div
className={clsx([styles.radioGroup, themed && styles.themed, className])}
ref={radioGroupRef}
{...props}
>
<legend>{title}</legend>
{description && <p className={styles.subtitle}>{description}</p>}
<div className={styles.options}>
{options?.map((option) => {
const id = `${name}_${option.value}`;
const checked = String(selectedValue) === String(option.value);
return (
<div
className={styles.option}
key={id}
data-testid="radio-option-container"
>
<input
type="radio"
name={name}
id={id}
onChange={handleChange}
value={option.value}
aria-checked={checked}
disabled={disabled || option.disabled}
/>
<div className={styles.content}>
<label className={styles.label} htmlFor={id}>
{option.label}
</label>
{option.description && (
<p className={styles.description}>{option.description}</p>
)}
</div>
</div>
);
})}
</div>
</div>
);
};
+545
View File
@@ -0,0 +1,545 @@
import clsx from "clsx";
import groupBy from "lodash/groupBy";
import {
ChangeEvent,
CSSProperties,
FC,
SelectHTMLAttributes,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
FeatureChoiceOption,
HtmlSelectOption,
HtmlSelectOptionGroup,
} from "@dndbeyond/character-rules-engine";
import Check from "@dndbeyond/fontawesome-cache/svgs/regular/check.svg";
import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.svg";
import { useIsVisible } from "@dndbeyond/ttui/hooks/useIsVisible";
import styles from "./styles.module.css";
export interface SelectOption {
value: string | number;
label: string;
groupLabel?: string;
index: number;
}
export interface SelectProps
extends Omit<
SelectHTMLAttributes<HTMLDivElement>,
"value" | "onChange" | "name" | "size"
> {
options: (FeatureChoiceOption | HtmlSelectOption | HtmlSelectOptionGroup)[];
value: string | number | null;
onChange?: (value: string | number) => void;
onChangeConfirm?: (
newValue: string,
oldValue: string,
accept: () => void,
reject: () => void
) => void;
placeholder?: string;
searchThreshold?: number | null;
searchPlaceholder?: string;
isHighlighted?: boolean;
name: string;
optionsWidth?: "medium" | "large" | "x-large";
hidePlaceholderOption?: boolean;
size?: "small" | "medium";
resetAfterChoice?: boolean;
}
export const Select: FC<SelectProps> = ({
className,
placeholder = "-- Choose an Option --",
name,
onChange,
onChangeConfirm,
options,
value,
disabled,
searchThreshold = 10,
searchPlaceholder = "Search...",
isHighlighted,
optionsWidth = "medium",
hidePlaceholderOption,
size = "medium",
resetAfterChoice,
...props
}) => {
const buttonRef = useRef<HTMLButtonElement>(null!);
const searchRef = useRef<HTMLInputElement>(null!);
const optionsRef = useRef<HTMLDivElement>(null!);
const optionsListRef = useRef<HTMLDivElement>(null!);
const isSearchActive = useRef(false);
const { ref: selectRef, isVisible, setIsVisible } = useIsVisible(false);
const [internalValue, setInternalValue] = useState(value || "");
const [query, setQuery] = useState("");
const [focused, setFocused] = useState(0);
const [buttonPosition, setButtonPosition] = useState<DOMRect | null>(null);
const getOptions = () => {
let counter = 1;
const optionArr: SelectOption[] = [];
options.forEach((option: FeatureChoiceOption | HtmlSelectOptionGroup) => {
if ("optGroupLabel" in option) {
option.options.forEach((opt: HtmlSelectOption) => {
optionArr.push({
label: opt.label || "",
value: opt.value,
groupLabel: option.optGroupLabel,
index: counter,
});
counter++;
});
} else {
optionArr.push({
label: option.label || "",
value: option.value,
index: counter,
});
counter++;
}
});
return optionArr;
};
const [results, setResults] = useState(getOptions());
const currentSelection = results.find(
(option) => String(option.value) === String(internalValue)
);
const scrollIntoView = useCallback(() => {
if (!optionsRef.current) return;
const optionList = optionsRef.current.querySelector(
`.${styles.optionList}`
);
if (!optionList) return;
let targetElement: Element | null = null;
// Look for the focused option first (including placeholder at index 0)
if (focused >= 0) {
targetElement = optionList.querySelector(`[data-index="${focused}"]`);
}
// If no focused option, look for the currently selected option
else {
targetElement = optionList.querySelector(`[aria-selected="true"]`);
}
// Scroll the target element into view within the dropdown container
if (targetElement) {
targetElement.scrollIntoView({
behavior: "auto",
block: "nearest",
inline: "nearest",
});
}
}, [focused]);
const buildOptionsHtml = (options: SelectOption[]) => [
...options.map((option) => (
<label
className={clsx([
styles.option,
focused === option.index && styles.focused,
])}
aria-selected={
(value === null && query === "" && option.index === 0) ||
String(internalValue) === String(option.value)
}
key={option.value}
data-index={option.index}
onMouseEnter={() => setFocused(option.index)}
>
<Check className={styles.check} aria-label={option.label} />
<input
type="radio"
name={name}
value={option.value}
onClick={() => handleClickOption(option.value)}
onChange={handleChange}
checked={value === option.value}
/>
{option.label}
</label>
)),
];
const getOptionsList = () => {
// Group items by groupLabel
const grouped = groupBy(results, "groupLabel");
// Turn grouped object into an array with groups and values
const entries = Object.entries(grouped);
// Map over grouped items to render
return entries.map(([groupName, options]) => {
// If there is a valid groupName
if (groupName !== "undefined") {
return (
<div className={styles.optionGroup} key={groupName}>
<label className={styles.groupLabel}>{groupName}</label>
{buildOptionsHtml(options)}
</div>
);
}
// If the groupName is "undefined"
return buildOptionsHtml(options);
});
};
const getOptionsPosition = (): CSSProperties | undefined => {
const maxHeight = 300; // Maximum height of the options dropdown
// If the button exists, set styles based on its position
if (buttonPosition && optionsListRef.current) {
// This logic checks to see if the options should be above or below the button
const buttonBottom = buttonPosition.y + buttonPosition.height;
// Get search height to account for that in the height calculation
const searchHeight = searchRef.current?.scrollHeight || 0;
// Get the height of the options list
const optionsListHeight = optionsListRef.current.scrollHeight;
// Calculate the height of the options list (capped at maxHeight)
const optionsHeight =
optionsListHeight + searchHeight > maxHeight
? maxHeight - searchHeight
: optionsListHeight + searchHeight;
// Determine if there is enoughy room below the button to show the entire options list
const isEnoughRoomBelow =
buttonBottom + optionsHeight < window.innerHeight;
const selectStyle = selectRef.current?.style;
selectStyle?.setProperty("--options-width", `${buttonPosition.width}px`);
selectStyle?.setProperty(
"--options-top",
isEnoughRoomBelow
? `${buttonBottom}px`
: `${buttonPosition.y - optionsHeight + 8}px`
);
selectStyle?.setProperty("--options-height", `${optionsHeight}px`);
}
return undefined;
};
const handleToggleVisibility = () => {
const isHidden = isVisible;
setIsVisible(!isHidden);
if (!isHidden) {
// Get the position of the button when it is clicked
if (buttonRef.current)
setButtonPosition(buttonRef.current.getBoundingClientRect());
// Reset state and scroll to selected option
setFocused(0);
setQuery("");
setResults(getOptions());
searchRef.current?.focus();
// Use setTimeout to ensure the DOM is updated before scrolling
setTimeout(() => {
scrollIntoView();
}, 0);
}
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
if (onChangeConfirm) {
// If a confirm modal is needed, use that handler
onChangeConfirm(
e.target.value,
String(internalValue),
() => {
if (onChange) {
onChange(e.target.value);
}
setInternalValue(e.target.value);
},
() => {}
);
}
// If no confirm modal is needed, use the normal handler
else {
if (onChange) onChange?.(e.target.value);
setInternalValue(e.target.value);
}
// Reset state
handleReset();
};
const handleClickOption = (value: string | number) => {
if (value === currentSelection?.value) {
handleReset();
}
};
const handleReset = () => {
if (resetAfterChoice) setInternalValue("");
setIsVisible(false);
setQuery("");
setResults(getOptions());
};
const handleSearch = (e) => {
// Reset results to default
if (!e.target.value) {
setResults(getOptions());
} else {
const filteredOptions = getOptions()
.filter((opt) =>
opt.label.toLowerCase().includes(e.target.value.toLowerCase())
)
.map((opt, index) => ({
...opt,
index,
}));
setResults(filteredOptions);
}
setQuery(e.target.value);
};
const handleKeypress = (e) => {
// Get the currently focused element in the DOM
const activeElement = document.activeElement as HTMLElement;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
if (isVisible) {
// Go to the next option
if (focused < results.length) {
setFocused(focused + 1);
}
// If there are no more options, go back to the top (placeholder at index 0)
else {
setFocused(0);
}
}
// If the button is focused and the dropdown is closed, open it
else if (activeElement === buttonRef.current) {
setIsVisible(true);
setFocused(currentSelection?.index || 0);
}
scrollIntoView();
break;
case "ArrowUp":
e.preventDefault();
if (isVisible) {
// Go to the previous option
if (focused > 0) {
setFocused(focused - 1);
}
// If there are no previous options, go back to the bottom
else {
setFocused(results.length);
}
}
// If the button is focused and the dropdown is closed, open it
else if (activeElement === buttonRef.current) {
setIsVisible(true);
setFocused(currentSelection?.index || results.length);
}
scrollIntoView();
break;
case "Enter":
e.preventDefault();
// If the dropdown is open, select the focused option
if (isVisible) {
if (focused === 0) {
// Handle the placeholder option (empty value)
handleChange({
target: { value: "" },
} as ChangeEvent<HTMLInputElement>);
buttonRef.current.focus();
} else {
const focusedOption = results.find((opt) => opt.index === focused);
if (focusedOption) {
handleChange({
target: { value: focusedOption.value.toString() },
} as ChangeEvent<HTMLInputElement>);
buttonRef.current.focus();
}
}
}
// If the button is focused and the dropdown is closed, open it
else if (activeElement === buttonRef.current) {
handleToggleVisibility();
}
scrollIntoView();
break;
case "Escape":
handleReset();
break;
case "Tab":
if (isVisible) {
// If the search is focused when tab is pressed go to item
if (document.activeElement === searchRef.current) {
e.preventDefault();
setFocused(currentSelection?.index || 0);
scrollIntoView();
searchRef.current.blur();
} else {
setFocused(0);
searchRef.current?.focus();
handleToggleVisibility();
}
}
break;
default:
break;
}
};
useEffect(() => {
const selectElement = selectRef.current;
if (!selectElement) return;
selectElement.addEventListener("keydown", handleKeypress);
return () => {
selectElement.removeEventListener("keydown", handleKeypress);
};
});
useEffect(
() => {
// keep the search active is searchThreshold is set and results are more than the threshold - this prevents the search from going away if the search results are less than the threshold after a query is entered
isSearchActive.current =
!!searchThreshold &&
searchThreshold >= 0 &&
results.length > searchThreshold;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(() => {
// If the select is visible, prevent the page from scrolling. This prevents us from having to
// recalculate the position of the option group on scroll.
document.body.style.overflowY = isVisible ? "hidden" : "unset";
getOptionsPosition();
// Scroll to the selected item when the dropdown becomes visible
if (isVisible) {
setTimeout(() => {
scrollIntoView();
}, 0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible, scrollIntoView]);
const optionsKey = useMemo(
() => options.map((o) => ("value" in o ? o.value : o.optGroupLabel)).join(","),
// eslint-disable-next-line react-hooks/exhaustive-deps
[options]
);
useEffect(() => {
if (!isSearchActive.current) {
setResults(getOptions());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [optionsKey]);
useEffect(() => {
if (currentSelection) {
setFocused(currentSelection.index);
}
}, [currentSelection]);
useEffect(() => {
setInternalValue(value || "");
}, [value]);
return (
<div
ref={selectRef}
className={clsx([
styles.select,
isHighlighted && styles.highlighted,
className,
])}
onClick={(e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}}
{...props}
>
<button
className={clsx([styles.button, styles[size]])}
id={`${name}Button`}
onClick={(e) => handleToggleVisibility()}
role="combobox"
aria-haspopup="listbox"
aria-controls={`${name}Dropdown`}
aria-labelledby={`${name}Label`}
aria-expanded={isVisible}
tabIndex={0}
disabled={disabled || false}
ref={buttonRef}
>
<span className={styles.buttonText}>
{currentSelection ? currentSelection?.label : placeholder}
</span>
<ChevronDown />
</button>
<div
ref={optionsRef}
className={clsx([styles.options, styles[optionsWidth]])}
role="group"
id={`${name}Dropdown`}
>
{isSearchActive.current && (
<input
className={styles.search}
type="search"
value={query}
onChange={handleSearch}
placeholder={searchPlaceholder}
ref={searchRef}
/>
)}
<div ref={optionsListRef} className={styles.optionList} role="menu">
{!!searchThreshold && query.length > 0 && results.length < 1 && (
<label className={styles.notFound}>
No results found for <strong>{query}</strong>
</label>
)}
{!query && !hidePlaceholderOption && (
<label
className={clsx([styles.option, focused === 0 && styles.focused])}
aria-selected={!internalValue && query === ""}
data-index={0}
onMouseEnter={() => setFocused(0)}
>
<Check className={styles.check} />
<input
type="radio"
name={name}
value={""}
onClick={() => handleClickOption("")}
onChange={handleChange}
checked={value === ""}
/>
{placeholder}
</label>
)}
{getOptionsList()}
</div>
</div>
</div>
);
};
+20 -8
View File
@@ -1,18 +1,25 @@
import clsx from "clsx"; import clsx from "clsx";
import { FC, Fragment, HTMLAttributes, ReactNode, useState } from "react"; import {
FC,
Fragment,
HTMLAttributes,
ReactNode,
useEffect,
useState,
} from "react";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { useCharacterTheme } from "~/contexts/CharacterTheme"; import { useCharacterTheme } from "~/contexts/CharacterTheme";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
interface TabFilterOption {
label: ReactNode;
content: ReactNode;
badge?: ReactNode;
}
export interface TabFilterProps extends HTMLAttributes<HTMLDivElement> { export interface TabFilterProps extends HTMLAttributes<HTMLDivElement> {
filters: filters: Array<TabFilterOption>;
| {
label: ReactNode;
content: ReactNode;
badge?: ReactNode;
}[];
showAllTab?: boolean; showAllTab?: boolean;
sharedChildren?: ReactNode; sharedChildren?: ReactNode;
} }
@@ -30,6 +37,11 @@ export const TabFilter: FC<TabFilterProps> = ({
const activeIndex = showAllTab ? activeFilter - 1 : activeFilter; const activeIndex = showAllTab ? activeFilter - 1 : activeFilter;
const nonEmptyFilters = filters.filter((f) => f.label !== ""); const nonEmptyFilters = filters.filter((f) => f.label !== "");
// Reset the active filter to the first tab when the filter options change
useEffect(() => {
setActiveFilter(0);
}, [filters.length]);
const getDefaultLabel = (label: ReactNode) => { const getDefaultLabel = (label: ReactNode) => {
if (typeof label === "string") return label; if (typeof label === "string") return label;
return uuidv4(); return uuidv4();
@@ -87,7 +99,7 @@ export const TabFilter: FC<TabFilterProps> = ({
); );
})} })}
</div> </div>
<div className={styles.content}> <div className={styles.content} data-scrollable-container="true">
{sharedChildren} {sharedChildren}
{!showAllTab || activeFilter !== 0 {!showAllTab || activeFilter !== 0
? nonEmptyFilters[activeIndex]?.content ? nonEmptyFilters[activeIndex]?.content
+73 -16
View File
@@ -1,11 +1,30 @@
import clsx from "clsx"; import clsx from "clsx";
import { FC, HTMLAttributes, ReactNode, useState } from "react"; import {
FC,
HTMLAttributes,
ReactNode,
useEffect,
useRef,
useState,
} from "react";
import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.svg"; import ChevronDown from "@dndbeyond/fontawesome-cache/svgs/solid/chevron-down.svg";
import { Popover } from "../Popover"; import { Popover } from "../Popover";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
/**
* This component renders a set of tabs with associated content panels.
* It supports different variants for tab behavior, including default,
* collapse, and toggle modes. Tabs can be limited in number of visible
* items, with overflow handled via a "More" dropdown menu.
*
** Variants:
* - default: Standard tab behavior where clicking a tab shows its content.
* - collapse: Always has a selected tab, but clicking it again collapses the content.
* - fullwidth: Tabs stretch to fill the container width.
*/
interface TabItem { interface TabItem {
label: ReactNode; label: ReactNode;
content: ReactNode; content: ReactNode;
@@ -15,44 +34,79 @@ interface TabItem {
interface TabListProps extends HTMLAttributes<HTMLElement> { interface TabListProps extends HTMLAttributes<HTMLElement> {
tabs: Array<TabItem | null>; tabs: Array<TabItem | null>;
variant?: "default" | "collapse" | "toggle"; variant?: "default" | "collapse" | "fullwidth";
defaultActiveId?: string; defaultActiveId?: string;
maxNavItemsShow?: number; maxNavItemsShow?: number;
onChangeTab?: (id: string) => void;
themed?: boolean;
forceShow?: boolean;
} }
export const TabList: FC<TabListProps> = ({ export const TabList: FC<TabListProps> = ({
className,
tabs, tabs,
defaultActiveId, defaultActiveId,
maxNavItemsShow = 7, maxNavItemsShow = 7,
variant = "default", variant = "default",
onChangeTab,
themed,
forceShow,
...props ...props
}) => { }) => {
const [activeTab, setActiveTab] = useState( const tabListRef = useRef<HTMLDivElement>(null!);
defaultActiveId ? defaultActiveId : tabs[0]?.id const [activeTab, setActiveTab] = useState(defaultActiveId || tabs[0]?.id);
const [isTabVisible, setIsTabVisible] = useState(
forceShow ?? activeTab !== ""
); );
const [isTabVisible, setIsTabVisible] = useState(activeTab !== "");
const tabsWithContent = tabs.filter((tab) => tab);
const visibleTabs = tabsWithContent.slice(0, maxNavItemsShow);
const hiddenTabs = tabsWithContent.slice(maxNavItemsShow);
const selectedTab = tabs.find((tab) => tab?.id === activeTab);
const isToggleable = variant === "collapse";
const handleTabClick = (id: string) => { const handleTabClick = (id: string) => {
if (id === activeTab && variant === "collapse") { if (id === activeTab && variant === "collapse") {
setIsTabVisible(!isTabVisible); setIsTabVisible(!isTabVisible);
} }
if (id === activeTab && variant === "toggle") {
setActiveTab("");
}
if (id !== activeTab) { if (id !== activeTab) {
setActiveTab(id); setActiveTab(id);
setIsTabVisible(true); setIsTabVisible(true);
if (onChangeTab) {
onChangeTab(id);
}
} }
}; };
const tabsWithContent = tabs.filter((tab) => tab);
const visibleTabs = tabsWithContent.slice(0, maxNavItemsShow); useEffect(
const hiddenTabs = tabsWithContent.slice(maxNavItemsShow); () => {
const selectedTab = tabs.find((tab) => tab?.id === activeTab); // Update CSS variable for number of tabs
const tabListStyles = tabListRef.current?.style;
if (tabListStyles) {
tabListStyles.setProperty("--tab-count", tabs.length.toString());
tabListStyles.setProperty(
"--tab-active-position",
tabs.indexOf(selectedTab as TabItem).toString()
);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[tabs.length, selectedTab]
);
return ( return (
<div {...props}> <div
className={clsx([
styles.tabList,
styles[variant],
themed && styles.themed,
className,
])}
ref={tabListRef}
{...props}
>
<menu className={styles.tabs}> <menu className={styles.tabs}>
{/* List of visible tabs */} {/* List of visible tabs */}
{visibleTabs.map((tab: TabItem) => ( {visibleTabs.map((tab: TabItem) => (
@@ -62,10 +116,13 @@ export const TabList: FC<TabListProps> = ({
role="radio" role="radio"
aria-checked={activeTab === tab.id} aria-checked={activeTab === tab.id}
onClick={() => handleTabClick(tab.id)} onClick={() => handleTabClick(tab.id)}
data-testid={tab.id}
> >
{tab.label} {tab.label}
{variant !== "default" && ( {isToggleable && (
<ChevronDown className={!isTabVisible ? styles.closed : ""} /> <ChevronDown
className={clsx([!isTabVisible && styles.closed])}
/>
)} )}
</button> </button>
</li> </li>
@@ -15,6 +15,7 @@ export interface TextareaProps
value?: string; value?: string;
onInputBlur: (textValue: string) => void; onInputBlur: (textValue: string) => void;
onInputKeyUp: () => void; onInputKeyUp: () => void;
placeholder?: string;
} }
/** /**
+13 -19
View File
@@ -1,12 +1,9 @@
import clsx from "clsx"; import clsx from "clsx";
import { useState, type FC, type HTMLAttributes } from "react"; import { useState, type FC, type HTMLAttributes } from "react";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
import { useSelector } from "react-redux";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
export interface ToggleProps export interface ToggleProps
extends Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> { extends Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> {
checked?: boolean; checked?: boolean;
color?: "primary" | "secondary" | "warning" | "error" | "info" | "success" | "themed"; color?: "primary" | "secondary" | "warning" | "error" | "info" | "success" | "themed";
onClick?: (isEnabled: boolean) => void; onClick?: (isEnabled: boolean) => void;
@@ -26,26 +23,23 @@ export const Toggle: FC<ToggleProps> = ({
onChangePromise, onChangePromise,
...props ...props
}) => { }) => {
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const [enabled, setEnabled] = useState(checked); const [enabled, setEnabled] = useState(checked);
const handleClick = (evt: React.MouseEvent): void => { const handleClick = (evt: React.MouseEvent): void => {
if (!isReadonly) { evt.stopPropagation();
evt.stopPropagation(); evt.nativeEvent.stopImmediatePropagation();
evt.nativeEvent.stopImmediatePropagation();
if (onChangePromise) { if (onChangePromise) {
onChangePromise( onChangePromise(
!enabled, !enabled,
() => { () => {
setEnabled(!enabled); setEnabled(!enabled);
}, },
() => {} () => {}
); );
} else if (onClick) { } else if (onClick) {
onClick(!enabled); onClick(!enabled);
setEnabled(!enabled); setEnabled(!enabled);
}
} }
}; };
+10 -3
View File
@@ -9,9 +9,9 @@ import {
RuleDataUtils, RuleDataUtils,
} from "@dndbeyond/character-rules-engine"; } from "@dndbeyond/character-rules-engine";
import { Select } from "~/components/Select";
import { useCharacterEngine } from "~/hooks/useCharacterEngine"; import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import XpBar from "~/tools/js/smartComponents/XpBar"; import XpBar from "~/tools/js/smartComponents/XpBar";
import { Select } from "~/tools/js/smartComponents/legacy";
import { Button } from "../Button"; import { Button } from "../Button";
import styles from "./styles.module.css"; import styles from "./styles.module.css";
@@ -144,15 +144,22 @@ export const XpManager: FC<XpManagerProps> = ({
</div> </div>
<div className={clsx([styles.controls, styles.withDivider])}> <div className={clsx([styles.controls, styles.withDivider])}>
<div className={styles.control}> <div className={styles.control}>
<label id="set-level" className={styles.label}> <label
id="set-level-label"
htmlFor="set-levelButton"
className={styles.label}
>
Set Level Set Level
</label> </label>
<Select <Select
aria-labelledby="set-level" className={styles.levelSelect}
aria-labelledby="set-level-label"
options={levelOptions} options={levelOptions}
onChange={handleChooseLevel} onChange={handleChooseLevel}
value={levelChosen} value={levelChosen}
placeholder={"--"} placeholder={"--"}
name="set-level"
searchThreshold={null}
/> />
</div> </div>
<div className={clsx([styles.control, styles.setXp])}> <div className={clsx([styles.control, styles.setXp])}>
+6 -2
View File
@@ -77,6 +77,11 @@ export const SourceCategoryDescription = {
export const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif"]; export const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/gif"];
export const FILE_SIZE_3MB = 3 * 1024 * 1024; export const FILE_SIZE_3MB = 3 * 1024 * 1024;
export const KETCH_RETRY_CONFIG = {
maxRetries: 10,
retryInterval: 500,
} as const;
/** /**
* Export constants from the rules engine to reduce the number of places where * Export constants from the rules engine to reduce the number of places where
* they're being imported. * they're being imported.
@@ -232,8 +237,6 @@ export const NoteTypeEnum = Constants.NoteTypeEnum;
export const NotificationTypeEnum = Constants.NotificationTypeEnum; export const NotificationTypeEnum = Constants.NotificationTypeEnum;
export const PoundsInTon = Constants.POUNDS_IN_TON; export const PoundsInTon = Constants.POUNDS_IN_TON;
export const PropertyList = Constants.PROPERTY_LIST; export const PropertyList = Constants.PROPERTY_LIST;
export const PartyInventorySharingStateEnum =
Constants.PartyInventorySharingStateEnum;
export const PreferenceAbilityScoreDisplayTypeEnum = export const PreferenceAbilityScoreDisplayTypeEnum =
Constants.PreferenceAbilityScoreDisplayTypeEnum; Constants.PreferenceAbilityScoreDisplayTypeEnum;
export const PreferenceEncumbranceTypeEnum = export const PreferenceEncumbranceTypeEnum =
@@ -243,6 +246,7 @@ export const PreferencePrivacyTypeEnum = Constants.PreferencePrivacyTypeEnum;
export const PreferenceProgressionTypeEnum = export const PreferenceProgressionTypeEnum =
Constants.PreferenceProgressionTypeEnum; Constants.PreferenceProgressionTypeEnum;
export const PreferenceSharingTypeEnum = Constants.PreferenceSharingTypeEnum; export const PreferenceSharingTypeEnum = Constants.PreferenceSharingTypeEnum;
export const PreferenceLongRestTypeEnum = Constants.PreferenceLongRestTypeEnum;
export const PrerequisiteSubTypeEnum = Constants.PrerequisiteSubTypeEnum; export const PrerequisiteSubTypeEnum = Constants.PrerequisiteSubTypeEnum;
export const PrerequisiteTypeEnum = Constants.PrerequisiteTypeEnum; export const PrerequisiteTypeEnum = Constants.PrerequisiteTypeEnum;
export const ProficiencyAdjustmentTypeEnum = export const ProficiencyAdjustmentTypeEnum =
@@ -62,16 +62,11 @@ export const CharacterThemeProvider = ({ children }) => {
__html: ` __html: `
:root { :root {
--theme-color: ${themeColor}; --theme-color: ${themeColor};
--theme-color-muted: color-mix(in srgb, var(--theme-color), var(--theme-background-solid) 30%);
--theme-background: ${backgroundColor}; --theme-background: ${backgroundColor};
--theme-background-solid: ${solidBackground}; --theme-background-solid: ${solidBackground};
--theme-contrast: var(--ttui_grey-${isDarkMode ? 50 : 900}); --theme-contrast: var(--ttui_grey-${isDarkMode ? 50 : 900});
--theme-transparent: color-mix(in srgb, var(--theme-color), transparent 60%); --theme-transparent: color-mix(in srgb, var(--theme-color), transparent 60%);
/* Update the character muted color for readability on dark mode */
${
isDarkMode ? "--character-muted-color: var(--ttui_grey-400);" : ""
}
}`, }`,
}} }}
/> />
+14 -11
View File
@@ -1,20 +1,23 @@
import { FC, createContext, useContext, useEffect, useState } from "react"; import {
FC,
PropsWithChildren,
createContext,
useContext,
useEffect,
useState,
} from "react";
import { toCamelCase } from "~/helpers/casing"; import { toCamelCase } from "~/helpers/casing";
import { getFeatureFlagsFromCharacterService } from "~/helpers/characterServiceApi"; import { getFeatureFlagsFromCharacterService } from "~/helpers/characterServiceApi";
const featureFlagsToGet = [ const featureFlagsToGet = [
"release-gate-ims",
"release-gate-gfs-blessings-ui",
"release-gate-character-sheet-tour", "release-gate-character-sheet-tour",
"release-gate-premade-characters", "circuit-breaker.datadog-rum-characters-app",
]; ];
export const defaultFlags = { export const defaultFlags = {
imsFlag: false,
gfsBlessingsUiFlag: false,
characterSheetTourFlag: false, characterSheetTourFlag: false,
premadeCharactersFlag: false, datadogRumCharactersAppFlag: false,
}; };
export type FeatureFlags = typeof defaultFlags; export type FeatureFlags = typeof defaultFlags;
@@ -25,7 +28,7 @@ export interface FeatureFlagContextType extends FeatureFlags {
export const FeatureFlagContext = createContext<FeatureFlagContextType>(null!); export const FeatureFlagContext = createContext<FeatureFlagContextType>(null!);
interface ProviderProps { interface ProviderProps extends PropsWithChildren {
defaultOverrides?: Partial<FeatureFlags>; defaultOverrides?: Partial<FeatureFlags>;
} }
@@ -44,12 +47,12 @@ export const FeatureFlagProvider: FC<ProviderProps> = ({
const formattedFlag = flag const formattedFlag = flag
// Remove `character-app` from the flag name // Remove `character-app` from the flag name
.replace(/character-app\./i, "") .replace(/character-app\./i, "")
// Remove `circuit-breaker` from the flag name
.replace(/circuit-breaker\./i, "")
// Remove `release-gate` from the flag name // Remove `release-gate` from the flag name
.replace(/release-gate\W/i, "") .replace(/release-gate\W/i, "")
// Remove `character-sheet` from flag name if not the tour flag // Remove `character-sheet` from flag name if not the tour flag
.replace(/character-sheet\W(?!tour)/i, "") .replace(/character-sheet\W(?!tour)/i, "");
// Change `2d` to `tactical` since var can't start with number
.replace(/2d/i, "tactical");
const key = toCamelCase(`${formattedFlag}Flag`); const key = toCamelCase(`${formattedFlag}Flag`);
return { ...acc, [key]: flags[flag] }; return { ...acc, [key]: flags[flag] };
+8 -2
View File
@@ -1,4 +1,10 @@
import { createContext, FC, useContext, useState } from "react"; import {
createContext,
FC,
PropsWithChildren,
useContext,
useState,
} from "react";
export interface FiltersContextType { export interface FiltersContextType {
showItemTypes: boolean; showItemTypes: boolean;
@@ -13,7 +19,7 @@ export interface FiltersContextType {
export const FiltersContext = createContext<FiltersContextType>(null!); export const FiltersContext = createContext<FiltersContextType>(null!);
export const FiltersProvider: FC = ({ children }) => { export const FiltersProvider: FC<PropsWithChildren> = ({ children }) => {
const [showItemTypes, setShowItemTypes] = useState(true); const [showItemTypes, setShowItemTypes] = useState(true);
const [showItemSourceCategories, setShowItemSourceCategories] = const [showItemSourceCategories, setShowItemSourceCategories] =
useState(true); useState(true);
+13 -2
View File
@@ -1,5 +1,12 @@
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { createContext, FC, useContext, useEffect, useState } from "react"; import {
createContext,
FC,
PropsWithChildren,
useContext,
useEffect,
useState,
} from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { import {
@@ -39,6 +46,7 @@ export interface PaneInfo {
) => void; ) => void;
paneHistoryPrevious: () => void; paneHistoryPrevious: () => void;
paneHistoryNext: () => void; paneHistoryNext: () => void;
resetActivePane: () => void;
} }
export interface SidebarContextType { export interface SidebarContextType {
@@ -48,7 +56,7 @@ export interface SidebarContextType {
export const SidebarContext = createContext<SidebarContextType>(null!); export const SidebarContext = createContext<SidebarContextType>(null!);
export const SidebarProvider: FC = ({ children }) => { export const SidebarProvider: FC<PropsWithChildren> = ({ children }) => {
const isMobile = useSelector(appEnvSelectors.getIsMobile); const isMobile = useSelector(appEnvSelectors.getIsMobile);
const sheetDimensions = useSelector(appEnvSelectors.getDimensions); const sheetDimensions = useSelector(appEnvSelectors.getDimensions);
@@ -64,6 +72,8 @@ export const SidebarProvider: FC = ({ children }) => {
const width: number = 340; const width: number = 340;
const resetActivePane = () => setActivePane(null);
//Create a new pane history //Create a new pane history
const paneHistoryStart = ( const paneHistoryStart = (
type: PaneComponentEnum, type: PaneComponentEnum,
@@ -161,6 +171,7 @@ export const SidebarProvider: FC = ({ children }) => {
paneHistoryPush, paneHistoryPush,
paneHistoryPrevious, paneHistoryPrevious,
paneHistoryNext, paneHistoryNext,
resetActivePane,
}, },
}} }}
> >
@@ -1,11 +1,8 @@
import createCache from "@emotion/cache"; import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react"; import { CacheProvider } from "@emotion/react";
import { ThemeProvider } from "@mui/material/styles";
import { useState, createContext, useEffect, useMemo } from "react"; import { useState, createContext, useEffect, useMemo } from "react";
import { prefixer } from "stylis"; import { prefixer } from "stylis";
import { getTheme } from "~/theme";
type PaletteOpt = "light" | "dark"; type PaletteOpt = "light" | "dark";
interface Manager { interface Manager {
@@ -77,12 +74,9 @@ export const ThemeManagerProvider = ({
/** /**
* End the hack * End the hack
*/ */
const theme = getTheme(manager.lightOrDark, manager.primary);
return ( return (
<ThemeManager.Provider value={{ manager, setManager }}> <ThemeManager.Provider value={{ manager, setManager }}>
<CacheProvider value={hack__cache}> <CacheProvider value={hack__cache}>{children}</CacheProvider>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</CacheProvider>
</ThemeManager.Provider> </ThemeManager.Provider>
); );
}; };
@@ -4,6 +4,7 @@ import {
PreferenceEncumbranceTypeEnum, PreferenceEncumbranceTypeEnum,
PreferenceHitPointTypeEnum, PreferenceHitPointTypeEnum,
PreferencePrivacyTypeEnum, PreferencePrivacyTypeEnum,
PreferenceLongRestTypeEnum,
PreferenceProgressionTypeEnum, PreferenceProgressionTypeEnum,
PreferenceSharingTypeEnum, PreferenceSharingTypeEnum,
SenseTypeEnum, SenseTypeEnum,
@@ -26,6 +27,7 @@ export const generateCharacterPreferences = (featureFlags: FeatureFlags) => {
const progressionType = PreferenceProgressionTypeEnum.MILESTONE; const progressionType = PreferenceProgressionTypeEnum.MILESTONE;
const sharingType = PreferenceSharingTypeEnum.LIMITED; const sharingType = PreferenceSharingTypeEnum.LIMITED;
const privacyType = PreferencePrivacyTypeEnum.CAMPAIGN_ONLY; const privacyType = PreferencePrivacyTypeEnum.CAMPAIGN_ONLY;
const longRestType = PreferenceLongRestTypeEnum.HALF;
return { return {
abilityScoreDisplayType, abilityScoreDisplayType,
@@ -38,6 +40,7 @@ export const generateCharacterPreferences = (featureFlags: FeatureFlags) => {
enforceMulticlassRules: true, enforceMulticlassRules: true,
hitPointType, hitPointType,
ignoreCoinWeight: true, ignoreCoinWeight: true,
longRestType,
primaryMovement, primaryMovement,
primarySense, primarySense,
privacyType, privacyType,
+66
View File
@@ -0,0 +1,66 @@
import {
SortByPropMap,
getSearchableTerms,
} from "~/state/selectors/characterUtils";
import { SortOrderEnum } from "~/types";
import { logListingSearchChanged } from "./analytics";
import { byProp } from "./sortUtils";
type KeyValuePair = { key: string; value: string | Array<string> };
export const keyValuePairsToQueryString = (
keyValuePairs: Array<KeyValuePair>
) =>
keyValuePairs
.map(({ key, value }) =>
Array.isArray(value)
? value.map((v) => `${key}=${v}`).join("&")
: `${key}=${value}`
)
.join("&");
export const queryListToMap = (queries) => {
return queries.reduce((queryMap, { key, value }) => {
queryMap[key] = value;
return queryMap;
}, {});
};
export const matchesSearchTerm =
(lowerSearch: string) => (searchableTerm: string) =>
searchableTerm.toLowerCase().includes(lowerSearch);
export const applyQueriesClientSide = (queryMap) => (json: any) => {
let data = json.data || [];
const { search, sortBy, sortOrder = SortOrderEnum.Ascending } = queryMap;
if (search) {
const matchesSearch = matchesSearchTerm(search.toLowerCase());
data = data.filter((character) =>
getSearchableTerms(character).some(matchesSearch)
);
// Logging this here will duplicate the log if a search term is used and then the sort is changed.
// If this was logged in MyCharactersListing.handleSearchChanged, then it wouldn't have the context of all data and would miss matches in some scenarios.
// This function is debounced to prevent spamming the logs as the user types.
logListingSearchChanged(data, matchesSearch);
}
const sortByPropSelector = SortByPropMap[sortBy];
if (sortByPropSelector) {
data = data.sort(byProp(sortByPropSelector));
if (sortOrder === SortOrderEnum.Descending) {
data.reverse();
}
}
return {
...json,
data,
};
};
+2 -5
View File
@@ -1,9 +1,6 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { import { AbilityManager } from "@dndbeyond/character-rules-engine/es";
AbilityManager,
FeaturesManager,
} from "@dndbeyond/character-rules-engine/es";
import { AttributesManagerContext } from "~/tools/js/Shared/managers/AttributesManagerContext"; import { AttributesManagerContext } from "~/tools/js/Shared/managers/AttributesManagerContext";
@@ -18,7 +15,7 @@ export function useAbilities() {
setAbilities(abilities); setAbilities(abilities);
} }
return FeaturesManager.subscribeToUpdates({ onUpdate }); return AbilityManager.subscribeToUpdates({ onUpdate });
}, [attributesManager, setAbilities]); }, [attributesManager, setAbilities]);
return abilities; return abilities;
+7 -5
View File
@@ -8,7 +8,6 @@ import {
ApiAdapterUtils as apiAdapterUtils, ApiAdapterUtils as apiAdapterUtils,
BackgroundUtils as backgroundUtils, BackgroundUtils as backgroundUtils,
CampaignUtils as campaignUtils, CampaignUtils as campaignUtils,
CampaignSettingUtils as campaignSettingUtils,
CharacterUtils as characterUtils, CharacterUtils as characterUtils,
ChoiceUtils as choiceUtils, ChoiceUtils as choiceUtils,
ClassFeatureUtils as classFeatureUtils, ClassFeatureUtils as classFeatureUtils,
@@ -124,7 +123,6 @@ export const useCharacterEngine = () => ({
baseFeats: useSelector(s.getBaseFeats), baseFeats: useSelector(s.getBaseFeats),
bonusSavingThrowModifiers: useSelector(s.getBonusSavingThrowModifiers), bonusSavingThrowModifiers: useSelector(s.getBonusSavingThrowModifiers),
campaign: useSelector(s.getCampaign), campaign: useSelector(s.getCampaign),
campaignSettings: useSelector(sds.getCampaignSettings),
carryingCapacity: useSelector(s.getCarryCapacity), carryingCapacity: useSelector(s.getCarryCapacity),
castableSpellSlotLevels: useSelector(s.getCastableSpellSlotLevels), castableSpellSlotLevels: useSelector(s.getCastableSpellSlotLevels),
castablePactMagicSlotLevels: useSelector(s.getCastablePactMagicSlotLevels), castablePactMagicSlotLevels: useSelector(s.getCastablePactMagicSlotLevels),
@@ -151,7 +149,7 @@ export const useCharacterEngine = () => ({
classSpells: useSelector(s.getClassSpells), classSpells: useSelector(s.getClassSpells),
classSpellInfo: useSelector(s.getClassSpellInfoLookup), classSpellInfo: useSelector(s.getClassSpellInfoLookup),
classSpellLists: useSelector(s.getClassSpellLists), classSpellLists: useSelector(s.getClassSpellLists),
classSpellListSpells: useSelector(s.getClassSpellListSpellsLookup), classSpellListSpellsLookup: useSelector(s.getClassSpellListSpellsLookup),
classes: useSelector(s.getClasses), classes: useSelector(s.getClasses),
classesModifiers: useSelector(s.getClassesModifiers), classesModifiers: useSelector(s.getClassesModifiers),
combinedMaxSpellSlotLevel: useSelector(s.getCombinedMaxSpellSlotLevel), combinedMaxSpellSlotLevel: useSelector(s.getCombinedMaxSpellSlotLevel),
@@ -170,6 +168,7 @@ export const useCharacterEngine = () => ({
creatureOwnerData: useSelector(s.getCreatureOwnerData), creatureOwnerData: useSelector(s.getCreatureOwnerData),
creatureRules: useSelector(s.getCreatureRules), creatureRules: useSelector(s.getCreatureRules),
creatures: useSelector(s.getCreatures), creatures: useSelector(s.getCreatures),
currencies: useSelector(s.getCurrencies),
currentCarriedWeightSpeed: useSelector(s.getCurrentCarriedWeightSpeed), currentCarriedWeightSpeed: useSelector(s.getCurrentCarriedWeightSpeed),
currentCarriedWeightType: useSelector(s.getCurrentCarriedWeightType), currentCarriedWeightType: useSelector(s.getCurrentCarriedWeightType),
currentLevel: useSelector(s.getCurrentLevel), currentLevel: useSelector(s.getCurrentLevel),
@@ -235,6 +234,7 @@ export const useCharacterEngine = () => ({
inventoryContainers: useSelector(s.getInventoryContainers), inventoryContainers: useSelector(s.getInventoryContainers),
inventoryLookup: useSelector(s.getInventoryLookup), inventoryLookup: useSelector(s.getInventoryLookup),
inventoryInfusion: useSelector(s.getInventoryInfusionLookup), inventoryInfusion: useSelector(s.getInventoryInfusionLookup),
itemPlans: useSelector(s.getAvailableItemPlans),
isSheetReady: useSelector(s.isCharacterSheetReady), isSheetReady: useSelector(s.isCharacterSheetReady),
isDead: useSelector(s.isDead), isDead: useSelector(s.isDead),
isMulticlassCharacter: useSelector(s.isMulticlassCharacter), isMulticlassCharacter: useSelector(s.isMulticlassCharacter),
@@ -252,7 +252,9 @@ export const useCharacterEngine = () => ({
levelSpells: useSelector(s.getLevelSpells), levelSpells: useSelector(s.getLevelSpells),
lifestyle: useSelector(s.getLifestyle), lifestyle: useSelector(s.getLifestyle),
loadSpecies: useSelector(api.makeLoadAvailableRaces), loadSpecies: useSelector(api.makeLoadAvailableRaces),
longRestText: useSelector(sds.getLongRestText),
maxPactMagicSlotLevel: useSelector(s.getMaxPactMagicSlotLevel), maxPactMagicSlotLevel: useSelector(s.getMaxPactMagicSlotLevel),
maxReplicatedItemsCount: useSelector(s.getMaxReplicatedItemsCount),
maxSpellSlotLevel: useSelector(s.getMaxSpellSlotLevel), maxSpellSlotLevel: useSelector(s.getMaxSpellSlotLevel),
miscModifiers: useSelector(s.getMiscModifiers), miscModifiers: useSelector(s.getMiscModifiers),
modifierData: useSelector(s.getModifierData), modifierData: useSelector(s.getModifierData),
@@ -279,7 +281,7 @@ export const useCharacterEngine = () => ({
partyInfo: useSelector(sds.getPartyInfo), partyInfo: useSelector(sds.getPartyInfo),
partyInventory: useSelector(s.getPartyInventory), partyInventory: useSelector(s.getPartyInventory),
partyInventoryContainers: useSelector(s.getPartyInventoryContainers), partyInventoryContainers: useSelector(s.getPartyInventoryContainers),
partyInventoryItem: useSelector(s.getPartyInventoryLookup), partyInventoryLookup: useSelector(s.getPartyInventoryLookup),
passiveInsight: useSelector(s.getPassiveInsight), passiveInsight: useSelector(s.getPassiveInsight),
passiveInvestigation: useSelector(s.getPassiveInvestigation), passiveInvestigation: useSelector(s.getPassiveInvestigation),
passivePerception: useSelector(s.getPassivePerception), passivePerception: useSelector(s.getPassivePerception),
@@ -294,6 +296,7 @@ export const useCharacterEngine = () => ({
playerName: useSelector(s.getUsername), playerName: useSelector(s.getUsername),
preferences: useSelector(s.getCharacterPreferences), preferences: useSelector(s.getCharacterPreferences),
prerequisiteData: useSelector(s.getPrerequisiteData), prerequisiteData: useSelector(s.getPrerequisiteData),
premadeInfo: useSelector(char.getPremadeInfo),
processedInitiative: useSelector(s.getProcessedInitiative), processedInitiative: useSelector(s.getProcessedInitiative),
proficiency: useSelector(s.getProficiencyLookup), proficiency: useSelector(s.getProficiencyLookup),
proficiencyBonus: useSelector(s.getProficiencyBonus), proficiencyBonus: useSelector(s.getProficiencyBonus),
@@ -370,7 +373,6 @@ export const useCharacterEngine = () => ({
apiAdapterUtils, apiAdapterUtils,
backgroundUtils, backgroundUtils,
campaignUtils, campaignUtils,
campaignSettingUtils,
characterUtils, characterUtils,
choiceUtils, choiceUtils,
classFeatureUtils, classFeatureUtils,
+2 -1
View File
@@ -1,8 +1,9 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { characterSelectors } from "@dndbeyond/character-rules-engine/es"; import { characterSelectors } from "@dndbeyond/character-rules-engine/es";
import { useDispatch } from "~/hooks/useDispatch";
import { toastMessageActions } from "~/tools/js/Shared/actions"; import { toastMessageActions } from "~/tools/js/Shared/actions";
import { import {
+59
View File
@@ -0,0 +1,59 @@
import { useCallback } from "react";
import { KETCH_RETRY_CONFIG } from "~/constants";
import { KetchConsentResponse } from "~/types";
export const useConsent = () => {
const setupAnalyticsConsentListener = useCallback(
(onConsentChange: (hasConsent: boolean) => void) => {
let retries = 0;
let timeoutId: ReturnType<typeof setTimeout> | null = null;
if (process.env.NODE_ENV === "development") {
onConsentChange(true);
return () => {
// noop cleanup for dev
};
}
const trySetup = () => {
if ("window" in globalThis && window.ketch) {
try {
window.ketch(
"on",
"consent",
(consentData: KetchConsentResponse) => {
onConsentChange(Boolean(consentData.purposes?.analytics));
}
);
} catch (error) {
console.error("Failed to setup Ketch consent listener:", error);
onConsentChange(false);
}
} else if (retries < KETCH_RETRY_CONFIG.maxRetries) {
retries++;
timeoutId = setTimeout(trySetup, KETCH_RETRY_CONFIG.retryInterval);
} else {
console.warn(
`Failed to initialize consent management after ${KETCH_RETRY_CONFIG.maxRetries} retries`
);
onConsentChange(false);
}
};
trySetup();
// Return cleanup function to cancel pending retries
return () => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
};
},
[]
);
return {
setupAnalyticsConsentListener,
};
};
+91
View File
@@ -0,0 +1,91 @@
import { datadogRum } from "@datadog/browser-rum";
import { useEffect, useState } from "react";
import type { User } from "@dndbeyond/authentication-lib-js";
import config from "~/config";
import { useConsent } from "~/hooks/useConsent";
const isDndbeyondUrl = (url: string): boolean => {
try {
const { protocol, hostname } = new URL(url);
return (
protocol === "https:" &&
(hostname === "dndbeyond.com" || hostname.endsWith(".dndbeyond.com"))
);
} catch {
return false;
}
};
export const useDatadogRum = (user: User | null) => {
const [hasConsent, setHasConsent] = useState(false);
const [isInitialized, setIsInitialized] = useState(false);
const environment = config.environment;
const version = config.version.replace(/@dndbeyond\/character-app@/, ""); // example: 1.70.116
const { setupAnalyticsConsentListener } = useConsent();
useEffect(() => {
const unsubscribe = setupAnalyticsConsentListener(setHasConsent);
return () => {
unsubscribe();
};
}, [setupAnalyticsConsentListener]);
useEffect(() => {
if (datadogRum.getInternalContext()) {
return;
}
try {
datadogRum.init({
applicationId: "2cfa3248-a632-4f5b-bf9e-aafa18dc69ae", // Not a secret, safe to include in client code
clientToken: "pub940bbbc614a4ae3ab0c872fdad597554", // Not a secret, safe to include in client code
site: "datadoghq.com",
service: "characters-app",
env: environment === "production" ? "live" : environment,
version: `${version}`,
sessionSampleRate: environment === "production" ? 10 : 100,
sessionReplaySampleRate: environment === "production" ? 1 : 10,
traceContextInjection: "sampled",
traceSampleRate: 10,
trackingConsent: "not-granted", // Start with no consent, will update when consent status is known
trackUserInteractions: true,
trackResources: true,
trackLongTasks: true,
defaultPrivacyLevel: "mask-user-input",
allowedTracingUrls: [isDndbeyondUrl],
});
setIsInitialized(true);
} catch (error) {
console.warn("Datadog RUM failed to initialize:", error);
}
}, [environment]);
useEffect(() => {
if (!isInitialized) {
return;
}
if (!user) {
datadogRum.clearUser();
return;
}
datadogRum.setUser({
id: `${user.id ?? ""}`,
name: `${user.name ?? ""}`,
sub_tier: `${user.subscriptionTier ?? ""}`,
});
}, [user, isInitialized]);
useEffect(() => {
if (!isInitialized) {
return;
}
datadogRum.setTrackingConsent(hasConsent ? "granted" : "not-granted");
}, [hasConsent, isInitialized]);
};
+7
View File
@@ -0,0 +1,7 @@
import { useDispatch as useReactReduxDispatch } from "react-redux";
import { AnyAction, Dispatch } from "redux";
// TODO: We are typing useDispatch this way to work around issues with UnknownAction, the type that was introduced in Redux 5.
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
export const useDispatch =
useReactReduxDispatch.withTypes<Dispatch<AnyAction>>();
@@ -1,33 +1,34 @@
import { HTMLAttributes, useState } from "react"; import { HTMLAttributes, JSX, useState } from "react";
import styles from '../../styles/errors.module.css';
import styles from "../../styles/errors.module.css";
export interface ErrorHandlerOptions { export interface ErrorHandlerOptions {
initialState: boolean; initialState: boolean;
errMsg: string; errMsg: string;
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>; errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
}; }
export interface ErrorHandler { export interface ErrorHandler {
showError: boolean; showError: boolean;
setShowError: (value: boolean) => void; setShowError: (value: boolean) => void;
ErrorMessage: () => JSX.Element; ErrorMessage: () => JSX.Element;
}; }
export const useErrorHandling = ( export const useErrorHandling = (
initialState: boolean, initialState: boolean,
errMsg: string, errMsg: string,
errorMessageAttributes: HTMLAttributes<HTMLDivElement> | null = null errorMessageAttributes: HTMLAttributes<HTMLDivElement> | null = null
): ErrorHandler => { ): ErrorHandler => {
const [showError, setShowError] = useState(initialState); const [showError, setShowError] = useState(initialState);
const ErrorMessage = (): JSX.Element => ( const ErrorMessage = (): JSX.Element => (
<div className={styles.inputError} {...errorMessageAttributes}> <div className={styles.inputError} {...errorMessageAttributes}>
{errMsg} {errMsg}
</div> </div>
); );
return { return {
showError, showError,
setShowError, setShowError,
ErrorMessage, ErrorMessage,
}; };
}; };
@@ -1,49 +1,49 @@
import { HTMLAttributes } from "react"; import { HTMLAttributes, JSX } from "react";
import { useErrorHandling } from "./useErrorHandling"; import { useErrorHandling } from "./useErrorHandling";
export interface MaxLengthErrorHandler { export interface MaxLengthErrorHandler {
handleMaxLengthErrorMsg: (value: string) => void; handleMaxLengthErrorMsg: (value: string) => void;
hideError: () => void; hideError: () => void;
MaxLengthErrorMessage: () => JSX.Element; MaxLengthErrorMessage: () => JSX.Element;
}; }
export const useMaxLengthErrorHandling = ( export const useMaxLengthErrorHandling = (
initialState: boolean, initialState: boolean,
maxLength: number | null, maxLength: number | null,
errMsg: string = "", errMsg: string = "",
errorMessageAttributes?: HTMLAttributes<HTMLDivElement> errorMessageAttributes?: HTMLAttributes<HTMLDivElement>
): MaxLengthErrorHandler => { ): MaxLengthErrorHandler => {
errMsg ||= `The max length is ${maxLength} characters.`; errMsg ||= `The max length is ${maxLength} characters.`;
const { const { showError, setShowError, ErrorMessage } = useErrorHandling(
showError, initialState,
setShowError, errMsg,
ErrorMessage, errorMessageAttributes
} = useErrorHandling(initialState, errMsg, errorMessageAttributes); );
const handleMaxLengthErrorMsg = (value: string): void => { const handleMaxLengthErrorMsg = (value: string): void => {
if (!maxLength) { if (!maxLength) {
// Skip if maxLength is 0 or not set. // Skip if maxLength is 0 or not set.
return; return;
}
const isTooLong = (value?.length ?? 0) >= (maxLength ?? 0);
if (isTooLong !== showError) {
setShowError(isTooLong);
}
};
const hideError = () => {
setShowError(false);
} }
const MaxLengthErrorMessage = (): JSX.Element => showError && errMsg const isTooLong = (value?.length ?? 0) >= (maxLength ?? 0);
? (<ErrorMessage />) if (isTooLong !== showError) {
: <></>; setShowError(isTooLong);
}
};
return { const hideError = () => {
handleMaxLengthErrorMsg, setShowError(false);
MaxLengthErrorMessage, };
hideError
}; const MaxLengthErrorMessage = (): JSX.Element =>
} showError && errMsg ? <ErrorMessage /> : <></>;
return {
handleMaxLengthErrorMsg,
MaxLengthErrorMessage,
hideError,
};
};
+2 -5
View File
@@ -1,9 +1,6 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { import { ExtraManager } from "@dndbeyond/character-rules-engine/es";
ExtraManager,
FeaturesManager,
} from "@dndbeyond/character-rules-engine/es";
import { ExtrasManagerContext } from "~/tools/js/Shared/managers/ExtrasManagerContext"; import { ExtrasManagerContext } from "~/tools/js/Shared/managers/ExtrasManagerContext";
@@ -19,7 +16,7 @@ export function useExtras() {
const onUpdate = () => { const onUpdate = () => {
setExtras(extrasManager.getCharacterExtraManagers()); setExtras(extrasManager.getCharacterExtraManagers());
}; };
return FeaturesManager.subscribeToUpdates({ onUpdate }); return ExtraManager.subscribeToUpdates({ onUpdate });
}, [extrasManager]); }, [extrasManager]);
return extras; return extras;
+2 -1
View File
@@ -1,5 +1,5 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { import {
ApiAdapterUtils, ApiAdapterUtils,
@@ -9,6 +9,7 @@ import {
syncTransactionActions, syncTransactionActions,
} from "@dndbeyond/character-rules-engine"; } from "@dndbeyond/character-rules-engine";
import { useDispatch } from "~/hooks/useDispatch";
import { toastMessageActions } from "~/tools/js/Shared/actions"; import { toastMessageActions } from "~/tools/js/Shared/actions";
export interface ExportPdfData { export interface ExportPdfData {
+1
View File
@@ -11,6 +11,7 @@ export interface AppUser extends User {
roles: Array<string>; roles: Array<string>;
subscription: string; subscription: string;
subscriptionTier: string; subscriptionTier: string;
avatarUrl?: string;
} }
/** /**
* AppUserState has three states: * AppUserState has three states:
+26 -19
View File
@@ -1,18 +1,22 @@
import ReactDOM from "react-dom"; import "core-js/stable";
import { QueryClient, QueryClientProvider } from "react-query";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { FontProvider } from "@dndbeyond/ftui-components";
import "@dndbeyond/ttui/styles/palette.css"; import "@dndbeyond/ttui/styles/palette.css";
import "@dndbeyond/ttui/styles/typography.css";
import wayOfLight from "@dndbeyond/ttui/themes/wayOfLight"; import wayOfLight from "@dndbeyond/ttui/themes/wayOfLight";
import { Layout } from "./components/Layout"; import { Layout } from "./components/Layout";
import { AuthProvider } from "./contexts/Authentication"; import config from "./config";
import { FeatureFlagProvider } from "./contexts/FeatureFlag"; import { AuthProvider, useAuth } from "./contexts/Authentication";
import { FeatureFlagProvider, useFeatureFlags } from "./contexts/FeatureFlag";
import { FiltersProvider } from "./contexts/Filters"; import { FiltersProvider } from "./contexts/Filters";
import { HeadContextProvider } from "./contexts/Head"; import { HeadContextProvider } from "./contexts/Head";
import { ThemeManagerProvider } from "./contexts/ThemeManager"; import { ThemeManagerProvider } from "./contexts/ThemeManager";
import { reportWebVitals } from "./helpers/reportWebVitals"; import { reportWebVitals } from "./helpers/reportWebVitals";
import { useDatadogRum } from "./hooks/useDatadogRum";
import { Routes } from "./routes"; import { Routes } from "./routes";
import "./styles/global.css"; import "./styles/global.css";
import "./styles/index.scss"; import "./styles/index.scss";
@@ -24,18 +28,19 @@ const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 60 * 60 * 1000 } }, defaultOptions: { queries: { staleTime: 60 * 60 * 1000 } },
}); });
const fonts = [ const DatadogGate = () => {
{ const { datadogRumCharactersAppFlag } = useFeatureFlags();
family: "Tiamat Condensed SC", return datadogRumCharactersAppFlag ? <DatadogBootstrap /> : null;
type: "woff2", };
url: "https://www.dndbeyond.com/fonts/tiamatcondensedsc-regular-webfont.woff2",
}, const DatadogBootstrap = () => {
{ const user = useAuth();
family: "MrsEavesSmallCaps", useDatadogRum(user ?? null);
type: "truetype",
url: "https://www.dndbeyond.com/content/skins/waterdeep/fonts/MrsEavesSmallCaps.ttf", return null;
}, };
];
const environment = config.environment;
const App = () => ( const App = () => (
<> <>
@@ -43,10 +48,10 @@ const App = () => (
<BrowserRouter> <BrowserRouter>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ThemeManagerProvider manager={{ lightOrDark: "light" }}> <ThemeManagerProvider manager={{ lightOrDark: "light" }}>
<FontProvider fonts={fonts} />
<HeadContextProvider> <HeadContextProvider>
<AuthProvider> <AuthProvider>
<FeatureFlagProvider> <FeatureFlagProvider>
{environment !== "development" && <DatadogGate />}
<FiltersProvider> <FiltersProvider>
<Layout> <Layout>
<Routes /> <Routes />
@@ -61,7 +66,9 @@ const App = () => (
</> </>
); );
ReactDOM.render(<App />, document.getElementById("character-tools-target")); const root = createRoot(document.getElementById("character-tools-target")!);
root.render(<App />);
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))
-13
View File
@@ -1,13 +0,0 @@
"use strict";
require("./noConflict");
var _global = _interopRequireDefault(require("core-js/library/fn/global"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
if (_global["default"]._babelPolyfill && typeof console !== "undefined" && console.warn) {
console.warn("@babel/polyfill is loaded more than once on this page. This is probably not desirable/intended " + "and may have consequences if different versions of the polyfills are applied sequentially. " + "If you do need to load the polyfill more than once, use @babel/polyfill/noConflict " + "instead to bypass the warning.");
}
_global["default"]._babelPolyfill = true;
-29
View File
@@ -1,29 +0,0 @@
"use strict";
require("core-js/es6");
require("core-js/fn/array/includes");
require("core-js/fn/array/flat-map");
require("core-js/fn/string/pad-start");
require("core-js/fn/string/pad-end");
require("core-js/fn/string/trim-start");
require("core-js/fn/string/trim-end");
require("core-js/fn/symbol/async-iterator");
require("core-js/fn/object/get-own-property-descriptors");
require("core-js/fn/object/values");
require("core-js/fn/object/entries");
require("core-js/fn/promise/finally");
require("core-js/web");
require("regenerator-runtime/runtime");
-139
View File
@@ -1,139 +0,0 @@
require('../modules/es6.symbol');
require('../modules/es6.object.create');
require('../modules/es6.object.define-property');
require('../modules/es6.object.define-properties');
require('../modules/es6.object.get-own-property-descriptor');
require('../modules/es6.object.get-prototype-of');
require('../modules/es6.object.keys');
require('../modules/es6.object.get-own-property-names');
require('../modules/es6.object.freeze');
require('../modules/es6.object.seal');
require('../modules/es6.object.prevent-extensions');
require('../modules/es6.object.is-frozen');
require('../modules/es6.object.is-sealed');
require('../modules/es6.object.is-extensible');
require('../modules/es6.object.assign');
require('../modules/es6.object.is');
require('../modules/es6.object.set-prototype-of');
require('../modules/es6.object.to-string');
require('../modules/es6.function.bind');
require('../modules/es6.function.name');
require('../modules/es6.function.has-instance');
require('../modules/es6.parse-int');
require('../modules/es6.parse-float');
require('../modules/es6.number.constructor');
require('../modules/es6.number.to-fixed');
require('../modules/es6.number.to-precision');
require('../modules/es6.number.epsilon');
require('../modules/es6.number.is-finite');
require('../modules/es6.number.is-integer');
require('../modules/es6.number.is-nan');
require('../modules/es6.number.is-safe-integer');
require('../modules/es6.number.max-safe-integer');
require('../modules/es6.number.min-safe-integer');
require('../modules/es6.number.parse-float');
require('../modules/es6.number.parse-int');
require('../modules/es6.math.acosh');
require('../modules/es6.math.asinh');
require('../modules/es6.math.atanh');
require('../modules/es6.math.cbrt');
require('../modules/es6.math.clz32');
require('../modules/es6.math.cosh');
require('../modules/es6.math.expm1');
require('../modules/es6.math.fround');
require('../modules/es6.math.hypot');
require('../modules/es6.math.imul');
require('../modules/es6.math.log10');
require('../modules/es6.math.log1p');
require('../modules/es6.math.log2');
require('../modules/es6.math.sign');
require('../modules/es6.math.sinh');
require('../modules/es6.math.tanh');
require('../modules/es6.math.trunc');
require('../modules/es6.string.from-code-point');
require('../modules/es6.string.raw');
require('../modules/es6.string.trim');
require('../modules/es6.string.iterator');
require('../modules/es6.string.code-point-at');
require('../modules/es6.string.ends-with');
require('../modules/es6.string.includes');
require('../modules/es6.string.repeat');
require('../modules/es6.string.starts-with');
require('../modules/es6.string.anchor');
require('../modules/es6.string.big');
require('../modules/es6.string.blink');
require('../modules/es6.string.bold');
require('../modules/es6.string.fixed');
require('../modules/es6.string.fontcolor');
require('../modules/es6.string.fontsize');
require('../modules/es6.string.italics');
require('../modules/es6.string.link');
require('../modules/es6.string.small');
require('../modules/es6.string.strike');
require('../modules/es6.string.sub');
require('../modules/es6.string.sup');
require('../modules/es6.date.now');
require('../modules/es6.date.to-json');
require('../modules/es6.date.to-iso-string');
require('../modules/es6.date.to-string');
require('../modules/es6.date.to-primitive');
require('../modules/es6.array.is-array');
require('../modules/es6.array.from');
require('../modules/es6.array.of');
require('../modules/es6.array.join');
require('../modules/es6.array.slice');
require('../modules/es6.array.sort');
require('../modules/es6.array.for-each');
require('../modules/es6.array.map');
require('../modules/es6.array.filter');
require('../modules/es6.array.some');
require('../modules/es6.array.every');
require('../modules/es6.array.reduce');
require('../modules/es6.array.reduce-right');
require('../modules/es6.array.index-of');
require('../modules/es6.array.last-index-of');
require('../modules/es6.array.copy-within');
require('../modules/es6.array.fill');
require('../modules/es6.array.find');
require('../modules/es6.array.find-index');
require('../modules/es6.array.species');
require('../modules/es6.array.iterator');
require('../modules/es6.regexp.constructor');
require('../modules/es6.regexp.exec');
require('../modules/es6.regexp.to-string');
require('../modules/es6.regexp.flags');
require('../modules/es6.regexp.match');
require('../modules/es6.regexp.replace');
require('../modules/es6.regexp.search');
require('../modules/es6.regexp.split');
require('../modules/es6.promise');
require('../modules/es6.map');
require('../modules/es6.set');
require('../modules/es6.weak-map');
require('../modules/es6.weak-set');
require('../modules/es6.typed.array-buffer');
require('../modules/es6.typed.data-view');
require('../modules/es6.typed.int8-array');
require('../modules/es6.typed.uint8-array');
require('../modules/es6.typed.uint8-clamped-array');
require('../modules/es6.typed.int16-array');
require('../modules/es6.typed.uint16-array');
require('../modules/es6.typed.int32-array');
require('../modules/es6.typed.uint32-array');
require('../modules/es6.typed.float32-array');
require('../modules/es6.typed.float64-array');
require('../modules/es6.reflect.apply');
require('../modules/es6.reflect.construct');
require('../modules/es6.reflect.define-property');
require('../modules/es6.reflect.delete-property');
require('../modules/es6.reflect.enumerate');
require('../modules/es6.reflect.get');
require('../modules/es6.reflect.get-own-property-descriptor');
require('../modules/es6.reflect.get-prototype-of');
require('../modules/es6.reflect.has');
require('../modules/es6.reflect.is-extensible');
require('../modules/es6.reflect.own-keys');
require('../modules/es6.reflect.prevent-extensions');
require('../modules/es6.reflect.set');
require('../modules/es6.reflect.set-prototype-of');
module.exports = require('../modules/_core');
@@ -1,2 +0,0 @@
require('../../modules/es7.array.flat-map');
module.exports = require('../../modules/_core').Array.flatMap;
@@ -1,2 +0,0 @@
require('../../modules/es7.array.includes');
module.exports = require('../../modules/_core').Array.includes;
@@ -1,2 +0,0 @@
require('../../modules/es7.object.entries');
module.exports = require('../../modules/_core').Object.entries;
@@ -1,2 +0,0 @@
require('../../modules/es7.object.get-own-property-descriptors');
module.exports = require('../../modules/_core').Object.getOwnPropertyDescriptors;
@@ -1,2 +0,0 @@
require('../../modules/es7.object.values');
module.exports = require('../../modules/_core').Object.values;
@@ -1,4 +0,0 @@
'use strict';
require('../../modules/es6.promise');
require('../../modules/es7.promise.finally');
module.exports = require('../../modules/_core').Promise['finally'];
@@ -1,2 +0,0 @@
require('../../modules/es7.string.pad-end');
module.exports = require('../../modules/_core').String.padEnd;
@@ -1,2 +0,0 @@
require('../../modules/es7.string.pad-start');
module.exports = require('../../modules/_core').String.padStart;
@@ -1,2 +0,0 @@
require('../../modules/es7.string.trim-right');
module.exports = require('../../modules/_core').String.trimRight;
@@ -1,2 +0,0 @@
require('../../modules/es7.string.trim-left');
module.exports = require('../../modules/_core').String.trimLeft;
@@ -1,2 +0,0 @@
require('../../modules/es7.symbol.async-iterator');
module.exports = require('../../modules/_wks-ext').f('asyncIterator');
@@ -1,2 +0,0 @@
require('../modules/es7.global');
module.exports = require('../modules/_core').global;
@@ -1,4 +0,0 @@
module.exports = function (it) {
if (typeof it != 'function') throw TypeError(it + ' is not a function!');
return it;
};
@@ -1,5 +0,0 @@
var isObject = require('./_is-object');
module.exports = function (it) {
if (!isObject(it)) throw TypeError(it + ' is not an object!');
return it;
};
@@ -1,2 +0,0 @@
var core = module.exports = { version: '2.6.12' };
if (typeof __e == 'number') __e = core; // eslint-disable-line no-undef
@@ -1,20 +0,0 @@
// optional / simple context binding
var aFunction = require('./_a-function');
module.exports = function (fn, that, length) {
aFunction(fn);
if (that === undefined) return fn;
switch (length) {
case 1: return function (a) {
return fn.call(that, a);
};
case 2: return function (a, b) {
return fn.call(that, a, b);
};
case 3: return function (a, b, c) {
return fn.call(that, a, b, c);
};
}
return function (/* ...args */) {
return fn.apply(that, arguments);
};
};
@@ -1,4 +0,0 @@
// Thank's IE8 for his funny defineProperty
module.exports = !require('./_fails')(function () {
return Object.defineProperty({}, 'a', { get: function () { return 7; } }).a != 7;
});
@@ -1,7 +0,0 @@
var isObject = require('./_is-object');
var document = require('./_global').document;
// typeof document.createElement is 'object' in old IE
var is = isObject(document) && isObject(document.createElement);
module.exports = function (it) {
return is ? document.createElement(it) : {};
};
@@ -1,62 +0,0 @@
var global = require('./_global');
var core = require('./_core');
var ctx = require('./_ctx');
var hide = require('./_hide');
var has = require('./_has');
var PROTOTYPE = 'prototype';
var $export = function (type, name, source) {
var IS_FORCED = type & $export.F;
var IS_GLOBAL = type & $export.G;
var IS_STATIC = type & $export.S;
var IS_PROTO = type & $export.P;
var IS_BIND = type & $export.B;
var IS_WRAP = type & $export.W;
var exports = IS_GLOBAL ? core : core[name] || (core[name] = {});
var expProto = exports[PROTOTYPE];
var target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE];
var key, own, out;
if (IS_GLOBAL) source = name;
for (key in source) {
// contains in native
own = !IS_FORCED && target && target[key] !== undefined;
if (own && has(exports, key)) continue;
// export native or passed
out = own ? target[key] : source[key];
// prevent global pollution for namespaces
exports[key] = IS_GLOBAL && typeof target[key] != 'function' ? source[key]
// bind timers to global for call from export context
: IS_BIND && own ? ctx(out, global)
// wrap global constructors for prevent change them in library
: IS_WRAP && target[key] == out ? (function (C) {
var F = function (a, b, c) {
if (this instanceof C) {
switch (arguments.length) {
case 0: return new C();
case 1: return new C(a);
case 2: return new C(a, b);
} return new C(a, b, c);
} return C.apply(this, arguments);
};
F[PROTOTYPE] = C[PROTOTYPE];
return F;
// make static versions for prototype methods
})(out) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out;
// export proto methods to core.%CONSTRUCTOR%.methods.%NAME%
if (IS_PROTO) {
(exports.virtual || (exports.virtual = {}))[key] = out;
// export proto methods to core.%CONSTRUCTOR%.prototype.%NAME%
if (type & $export.R && expProto && !expProto[key]) hide(expProto, key, out);
}
}
};
// type bitmap
$export.F = 1; // forced
$export.G = 2; // global
$export.S = 4; // static
$export.P = 8; // proto
$export.B = 16; // bind
$export.W = 32; // wrap
$export.U = 64; // safe
$export.R = 128; // real proto method for `library`
module.exports = $export;
@@ -1,7 +0,0 @@
module.exports = function (exec) {
try {
return !!exec();
} catch (e) {
return true;
}
};
@@ -1,6 +0,0 @@
// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028
var global = module.exports = typeof window != 'undefined' && window.Math == Math
? window : typeof self != 'undefined' && self.Math == Math ? self
// eslint-disable-next-line no-new-func
: Function('return this')();
if (typeof __g == 'number') __g = global; // eslint-disable-line no-undef
@@ -1,4 +0,0 @@
var hasOwnProperty = {}.hasOwnProperty;
module.exports = function (it, key) {
return hasOwnProperty.call(it, key);
};
@@ -1,8 +0,0 @@
var dP = require('./_object-dp');
var createDesc = require('./_property-desc');
module.exports = require('./_descriptors') ? function (object, key, value) {
return dP.f(object, key, createDesc(1, value));
} : function (object, key, value) {
object[key] = value;
return object;
};
@@ -1,3 +0,0 @@
module.exports = !require('./_descriptors') && !require('./_fails')(function () {
return Object.defineProperty(require('./_dom-create')('div'), 'a', { get: function () { return 7; } }).a != 7;
});
@@ -1,3 +0,0 @@
module.exports = function (it) {
return typeof it === 'object' ? it !== null : typeof it === 'function';
};
@@ -1,16 +0,0 @@
var anObject = require('./_an-object');
var IE8_DOM_DEFINE = require('./_ie8-dom-define');
var toPrimitive = require('./_to-primitive');
var dP = Object.defineProperty;
exports.f = require('./_descriptors') ? Object.defineProperty : function defineProperty(O, P, Attributes) {
anObject(O);
P = toPrimitive(P, true);
anObject(Attributes);
if (IE8_DOM_DEFINE) try {
return dP(O, P, Attributes);
} catch (e) { /* empty */ }
if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported!');
if ('value' in Attributes) O[P] = Attributes.value;
return O;
};
@@ -1,8 +0,0 @@
module.exports = function (bitmap, value) {
return {
enumerable: !(bitmap & 1),
configurable: !(bitmap & 2),
writable: !(bitmap & 4),
value: value
};
};
@@ -1,12 +0,0 @@
// 7.1.1 ToPrimitive(input [, PreferredType])
var isObject = require('./_is-object');
// instead of the ES6 spec version, we didn't implement @@toPrimitive case
// and the second argument - flag - preferred type is a string
module.exports = function (it, S) {
if (!isObject(it)) return it;
var fn, val;
if (S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val;
if (typeof (fn = it.valueOf) == 'function' && !isObject(val = fn.call(it))) return val;
if (!S && typeof (fn = it.toString) == 'function' && !isObject(val = fn.call(it))) return val;
throw TypeError("Can't convert object to primitive value");
};
@@ -1,4 +0,0 @@
// https://github.com/tc39/proposal-global
var $export = require('./_export');
$export($export.G, { global: require('./_global') });
@@ -1,4 +0,0 @@
module.exports = function (it) {
if (typeof it != 'function') throw TypeError(it + ' is not a function!');
return it;
};
@@ -1,5 +0,0 @@
var cof = require('./_cof');
module.exports = function (it, msg) {
if (typeof it != 'number' && cof(it) != 'Number') throw TypeError(msg);
return +it;
};
@@ -1,7 +0,0 @@
// 22.1.3.31 Array.prototype[@@unscopables]
var UNSCOPABLES = require('./_wks')('unscopables');
var ArrayProto = Array.prototype;
if (ArrayProto[UNSCOPABLES] == undefined) require('./_hide')(ArrayProto, UNSCOPABLES, {});
module.exports = function (key) {
ArrayProto[UNSCOPABLES][key] = true;
};
@@ -1,8 +0,0 @@
'use strict';
var at = require('./_string-at')(true);
// `AdvanceStringIndex` abstract operation
// https://tc39.github.io/ecma262/#sec-advancestringindex
module.exports = function (S, index, unicode) {
return index + (unicode ? at(S, index).length : 1);
};
@@ -1,5 +0,0 @@
module.exports = function (it, Constructor, name, forbiddenField) {
if (!(it instanceof Constructor) || (forbiddenField !== undefined && forbiddenField in it)) {
throw TypeError(name + ': incorrect invocation!');
} return it;
};
@@ -1,5 +0,0 @@
var isObject = require('./_is-object');
module.exports = function (it) {
if (!isObject(it)) throw TypeError(it + ' is not an object!');
return it;
};
@@ -1,26 +0,0 @@
// 22.1.3.3 Array.prototype.copyWithin(target, start, end = this.length)
'use strict';
var toObject = require('./_to-object');
var toAbsoluteIndex = require('./_to-absolute-index');
var toLength = require('./_to-length');
module.exports = [].copyWithin || function copyWithin(target /* = 0 */, start /* = 0, end = @length */) {
var O = toObject(this);
var len = toLength(O.length);
var to = toAbsoluteIndex(target, len);
var from = toAbsoluteIndex(start, len);
var end = arguments.length > 2 ? arguments[2] : undefined;
var count = Math.min((end === undefined ? len : toAbsoluteIndex(end, len)) - from, len - to);
var inc = 1;
if (from < to && to < from + count) {
inc = -1;
from += count - 1;
to += count - 1;
}
while (count-- > 0) {
if (from in O) O[to] = O[from];
else delete O[to];
to += inc;
from += inc;
} return O;
};
@@ -1,15 +0,0 @@
// 22.1.3.6 Array.prototype.fill(value, start = 0, end = this.length)
'use strict';
var toObject = require('./_to-object');
var toAbsoluteIndex = require('./_to-absolute-index');
var toLength = require('./_to-length');
module.exports = function fill(value /* , start = 0, end = @length */) {
var O = toObject(this);
var length = toLength(O.length);
var aLen = arguments.length;
var index = toAbsoluteIndex(aLen > 1 ? arguments[1] : undefined, length);
var end = aLen > 2 ? arguments[2] : undefined;
var endPos = end === undefined ? length : toAbsoluteIndex(end, length);
while (endPos > index) O[index++] = value;
return O;
};
@@ -1,23 +0,0 @@
// false -> Array#indexOf
// true -> Array#includes
var toIObject = require('./_to-iobject');
var toLength = require('./_to-length');
var toAbsoluteIndex = require('./_to-absolute-index');
module.exports = function (IS_INCLUDES) {
return function ($this, el, fromIndex) {
var O = toIObject($this);
var length = toLength(O.length);
var index = toAbsoluteIndex(fromIndex, length);
var value;
// Array#includes uses SameValueZero equality algorithm
// eslint-disable-next-line no-self-compare
if (IS_INCLUDES && el != el) while (length > index) {
value = O[index++];
// eslint-disable-next-line no-self-compare
if (value != value) return true;
// Array#indexOf ignores holes, Array#includes - not
} else for (;length > index; index++) if (IS_INCLUDES || index in O) {
if (O[index] === el) return IS_INCLUDES || index || 0;
} return !IS_INCLUDES && -1;
};
};
@@ -1,44 +0,0 @@
// 0 -> Array#forEach
// 1 -> Array#map
// 2 -> Array#filter
// 3 -> Array#some
// 4 -> Array#every
// 5 -> Array#find
// 6 -> Array#findIndex
var ctx = require('./_ctx');
var IObject = require('./_iobject');
var toObject = require('./_to-object');
var toLength = require('./_to-length');
var asc = require('./_array-species-create');
module.exports = function (TYPE, $create) {
var IS_MAP = TYPE == 1;
var IS_FILTER = TYPE == 2;
var IS_SOME = TYPE == 3;
var IS_EVERY = TYPE == 4;
var IS_FIND_INDEX = TYPE == 6;
var NO_HOLES = TYPE == 5 || IS_FIND_INDEX;
var create = $create || asc;
return function ($this, callbackfn, that) {
var O = toObject($this);
var self = IObject(O);
var f = ctx(callbackfn, that, 3);
var length = toLength(self.length);
var index = 0;
var result = IS_MAP ? create($this, length) : IS_FILTER ? create($this, 0) : undefined;
var val, res;
for (;length > index; index++) if (NO_HOLES || index in self) {
val = self[index];
res = f(val, index, O);
if (TYPE) {
if (IS_MAP) result[index] = res; // map
else if (res) switch (TYPE) {
case 3: return true; // some
case 5: return val; // find
case 6: return index; // findIndex
case 2: result.push(val); // filter
} else if (IS_EVERY) return false; // every
}
}
return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : result;
};
};
@@ -1,28 +0,0 @@
var aFunction = require('./_a-function');
var toObject = require('./_to-object');
var IObject = require('./_iobject');
var toLength = require('./_to-length');
module.exports = function (that, callbackfn, aLen, memo, isRight) {
aFunction(callbackfn);
var O = toObject(that);
var self = IObject(O);
var length = toLength(O.length);
var index = isRight ? length - 1 : 0;
var i = isRight ? -1 : 1;
if (aLen < 2) for (;;) {
if (index in self) {
memo = self[index];
index += i;
break;
}
index += i;
if (isRight ? index < 0 : length <= index) {
throw TypeError('Reduce of empty array with no initial value');
}
}
for (;isRight ? index >= 0 : length > index; index += i) if (index in self) {
memo = callbackfn(memo, self[index], index, O);
}
return memo;
};
@@ -1,16 +0,0 @@
var isObject = require('./_is-object');
var isArray = require('./_is-array');
var SPECIES = require('./_wks')('species');
module.exports = function (original) {
var C;
if (isArray(original)) {
C = original.constructor;
// cross-realm fallback
if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined;
if (isObject(C)) {
C = C[SPECIES];
if (C === null) C = undefined;
}
} return C === undefined ? Array : C;
};
@@ -1,6 +0,0 @@
// 9.4.2.3 ArraySpeciesCreate(originalArray, length)
var speciesConstructor = require('./_array-species-constructor');
module.exports = function (original, length) {
return new (speciesConstructor(original))(length);
};
@@ -1,25 +0,0 @@
'use strict';
var aFunction = require('./_a-function');
var isObject = require('./_is-object');
var invoke = require('./_invoke');
var arraySlice = [].slice;
var factories = {};
var construct = function (F, len, args) {
if (!(len in factories)) {
for (var n = [], i = 0; i < len; i++) n[i] = 'a[' + i + ']';
// eslint-disable-next-line no-new-func
factories[len] = Function('F,a', 'return new F(' + n.join(',') + ')');
} return factories[len](F, args);
};
module.exports = Function.bind || function bind(that /* , ...args */) {
var fn = aFunction(this);
var partArgs = arraySlice.call(arguments, 1);
var bound = function (/* args... */) {
var args = partArgs.concat(arraySlice.call(arguments));
return this instanceof bound ? construct(fn, args.length, args) : invoke(fn, args, that);
};
if (isObject(fn.prototype)) bound.prototype = fn.prototype;
return bound;
};
@@ -1,23 +0,0 @@
// getting tag from 19.1.3.6 Object.prototype.toString()
var cof = require('./_cof');
var TAG = require('./_wks')('toStringTag');
// ES3 wrong here
var ARG = cof(function () { return arguments; }()) == 'Arguments';
// fallback for IE11 Script Access Denied error
var tryGet = function (it, key) {
try {
return it[key];
} catch (e) { /* empty */ }
};
module.exports = function (it) {
var O, T, B;
return it === undefined ? 'Undefined' : it === null ? 'Null'
// @@toStringTag case
: typeof (T = tryGet(O = Object(it), TAG)) == 'string' ? T
// builtinTag case
: ARG ? cof(O)
// ES3 arguments fallback
: (B = cof(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : B;
};
@@ -1,5 +0,0 @@
var toString = {}.toString;
module.exports = function (it) {
return toString.call(it).slice(8, -1);
};
@@ -1,144 +0,0 @@
'use strict';
var dP = require('./_object-dp').f;
var create = require('./_object-create');
var redefineAll = require('./_redefine-all');
var ctx = require('./_ctx');
var anInstance = require('./_an-instance');
var forOf = require('./_for-of');
var $iterDefine = require('./_iter-define');
var step = require('./_iter-step');
var setSpecies = require('./_set-species');
var DESCRIPTORS = require('./_descriptors');
var fastKey = require('./_meta').fastKey;
var validate = require('./_validate-collection');
var SIZE = DESCRIPTORS ? '_s' : 'size';
var getEntry = function (that, key) {
// fast case
var index = fastKey(key);
var entry;
if (index !== 'F') return that._i[index];
// frozen object case
for (entry = that._f; entry; entry = entry.n) {
if (entry.k == key) return entry;
}
};
module.exports = {
getConstructor: function (wrapper, NAME, IS_MAP, ADDER) {
var C = wrapper(function (that, iterable) {
anInstance(that, C, NAME, '_i');
that._t = NAME; // collection type
that._i = create(null); // index
that._f = undefined; // first entry
that._l = undefined; // last entry
that[SIZE] = 0; // size
if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
});
redefineAll(C.prototype, {
// 23.1.3.1 Map.prototype.clear()
// 23.2.3.2 Set.prototype.clear()
clear: function clear() {
for (var that = validate(this, NAME), data = that._i, entry = that._f; entry; entry = entry.n) {
entry.r = true;
if (entry.p) entry.p = entry.p.n = undefined;
delete data[entry.i];
}
that._f = that._l = undefined;
that[SIZE] = 0;
},
// 23.1.3.3 Map.prototype.delete(key)
// 23.2.3.4 Set.prototype.delete(value)
'delete': function (key) {
var that = validate(this, NAME);
var entry = getEntry(that, key);
if (entry) {
var next = entry.n;
var prev = entry.p;
delete that._i[entry.i];
entry.r = true;
if (prev) prev.n = next;
if (next) next.p = prev;
if (that._f == entry) that._f = next;
if (that._l == entry) that._l = prev;
that[SIZE]--;
} return !!entry;
},
// 23.2.3.6 Set.prototype.forEach(callbackfn, thisArg = undefined)
// 23.1.3.5 Map.prototype.forEach(callbackfn, thisArg = undefined)
forEach: function forEach(callbackfn /* , that = undefined */) {
validate(this, NAME);
var f = ctx(callbackfn, arguments.length > 1 ? arguments[1] : undefined, 3);
var entry;
while (entry = entry ? entry.n : this._f) {
f(entry.v, entry.k, this);
// revert to the last existing entry
while (entry && entry.r) entry = entry.p;
}
},
// 23.1.3.7 Map.prototype.has(key)
// 23.2.3.7 Set.prototype.has(value)
has: function has(key) {
return !!getEntry(validate(this, NAME), key);
}
});
if (DESCRIPTORS) dP(C.prototype, 'size', {
get: function () {
return validate(this, NAME)[SIZE];
}
});
return C;
},
def: function (that, key, value) {
var entry = getEntry(that, key);
var prev, index;
// change existing entry
if (entry) {
entry.v = value;
// create new entry
} else {
that._l = entry = {
i: index = fastKey(key, true), // <- index
k: key, // <- key
v: value, // <- value
p: prev = that._l, // <- previous entry
n: undefined, // <- next entry
r: false // <- removed
};
if (!that._f) that._f = entry;
if (prev) prev.n = entry;
that[SIZE]++;
// add to index
if (index !== 'F') that._i[index] = entry;
} return that;
},
getEntry: getEntry,
setStrong: function (C, NAME, IS_MAP) {
// add .keys, .values, .entries, [@@iterator]
// 23.1.3.4, 23.1.3.8, 23.1.3.11, 23.1.3.12, 23.2.3.5, 23.2.3.8, 23.2.3.10, 23.2.3.11
$iterDefine(C, NAME, function (iterated, kind) {
this._t = validate(iterated, NAME); // target
this._k = kind; // kind
this._l = undefined; // previous
}, function () {
var that = this;
var kind = that._k;
var entry = that._l;
// revert to the last existing entry
while (entry && entry.r) entry = entry.p;
// get next entry
if (!that._t || !(that._l = entry = entry ? entry.n : that._t._f)) {
// or finish the iteration
that._t = undefined;
return step(1);
}
// return step by kind
if (kind == 'keys') return step(0, entry.k);
if (kind == 'values') return step(0, entry.v);
return step(0, [entry.k, entry.v]);
}, IS_MAP ? 'entries' : 'values', !IS_MAP, true);
// add [@@species], 23.1.2.2, 23.2.2.2
setSpecies(NAME);
}
};
@@ -1,85 +0,0 @@
'use strict';
var redefineAll = require('./_redefine-all');
var getWeak = require('./_meta').getWeak;
var anObject = require('./_an-object');
var isObject = require('./_is-object');
var anInstance = require('./_an-instance');
var forOf = require('./_for-of');
var createArrayMethod = require('./_array-methods');
var $has = require('./_has');
var validate = require('./_validate-collection');
var arrayFind = createArrayMethod(5);
var arrayFindIndex = createArrayMethod(6);
var id = 0;
// fallback for uncaught frozen keys
var uncaughtFrozenStore = function (that) {
return that._l || (that._l = new UncaughtFrozenStore());
};
var UncaughtFrozenStore = function () {
this.a = [];
};
var findUncaughtFrozen = function (store, key) {
return arrayFind(store.a, function (it) {
return it[0] === key;
});
};
UncaughtFrozenStore.prototype = {
get: function (key) {
var entry = findUncaughtFrozen(this, key);
if (entry) return entry[1];
},
has: function (key) {
return !!findUncaughtFrozen(this, key);
},
set: function (key, value) {
var entry = findUncaughtFrozen(this, key);
if (entry) entry[1] = value;
else this.a.push([key, value]);
},
'delete': function (key) {
var index = arrayFindIndex(this.a, function (it) {
return it[0] === key;
});
if (~index) this.a.splice(index, 1);
return !!~index;
}
};
module.exports = {
getConstructor: function (wrapper, NAME, IS_MAP, ADDER) {
var C = wrapper(function (that, iterable) {
anInstance(that, C, NAME, '_i');
that._t = NAME; // collection type
that._i = id++; // collection id
that._l = undefined; // leak store for uncaught frozen objects
if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
});
redefineAll(C.prototype, {
// 23.3.3.2 WeakMap.prototype.delete(key)
// 23.4.3.3 WeakSet.prototype.delete(value)
'delete': function (key) {
if (!isObject(key)) return false;
var data = getWeak(key);
if (data === true) return uncaughtFrozenStore(validate(this, NAME))['delete'](key);
return data && $has(data, this._i) && delete data[this._i];
},
// 23.3.3.4 WeakMap.prototype.has(key)
// 23.4.3.4 WeakSet.prototype.has(value)
has: function has(key) {
if (!isObject(key)) return false;
var data = getWeak(key);
if (data === true) return uncaughtFrozenStore(validate(this, NAME)).has(key);
return data && $has(data, this._i);
}
});
return C;
},
def: function (that, key, value) {
var data = getWeak(anObject(key), true);
if (data === true) uncaughtFrozenStore(that).set(key, value);
else data[that._i] = value;
return that;
},
ufstore: uncaughtFrozenStore
};
@@ -1,85 +0,0 @@
'use strict';
var global = require('./_global');
var $export = require('./_export');
var redefine = require('./_redefine');
var redefineAll = require('./_redefine-all');
var meta = require('./_meta');
var forOf = require('./_for-of');
var anInstance = require('./_an-instance');
var isObject = require('./_is-object');
var fails = require('./_fails');
var $iterDetect = require('./_iter-detect');
var setToStringTag = require('./_set-to-string-tag');
var inheritIfRequired = require('./_inherit-if-required');
module.exports = function (NAME, wrapper, methods, common, IS_MAP, IS_WEAK) {
var Base = global[NAME];
var C = Base;
var ADDER = IS_MAP ? 'set' : 'add';
var proto = C && C.prototype;
var O = {};
var fixMethod = function (KEY) {
var fn = proto[KEY];
redefine(proto, KEY,
KEY == 'delete' ? function (a) {
return IS_WEAK && !isObject(a) ? false : fn.call(this, a === 0 ? 0 : a);
} : KEY == 'has' ? function has(a) {
return IS_WEAK && !isObject(a) ? false : fn.call(this, a === 0 ? 0 : a);
} : KEY == 'get' ? function get(a) {
return IS_WEAK && !isObject(a) ? undefined : fn.call(this, a === 0 ? 0 : a);
} : KEY == 'add' ? function add(a) { fn.call(this, a === 0 ? 0 : a); return this; }
: function set(a, b) { fn.call(this, a === 0 ? 0 : a, b); return this; }
);
};
if (typeof C != 'function' || !(IS_WEAK || proto.forEach && !fails(function () {
new C().entries().next();
}))) {
// create collection constructor
C = common.getConstructor(wrapper, NAME, IS_MAP, ADDER);
redefineAll(C.prototype, methods);
meta.NEED = true;
} else {
var instance = new C();
// early implementations not supports chaining
var HASNT_CHAINING = instance[ADDER](IS_WEAK ? {} : -0, 1) != instance;
// V8 ~ Chromium 40- weak-collections throws on primitives, but should return false
var THROWS_ON_PRIMITIVES = fails(function () { instance.has(1); });
// most early implementations doesn't supports iterables, most modern - not close it correctly
var ACCEPT_ITERABLES = $iterDetect(function (iter) { new C(iter); }); // eslint-disable-line no-new
// for early implementations -0 and +0 not the same
var BUGGY_ZERO = !IS_WEAK && fails(function () {
// V8 ~ Chromium 42- fails only with 5+ elements
var $instance = new C();
var index = 5;
while (index--) $instance[ADDER](index, index);
return !$instance.has(-0);
});
if (!ACCEPT_ITERABLES) {
C = wrapper(function (target, iterable) {
anInstance(target, C, NAME);
var that = inheritIfRequired(new Base(), target, C);
if (iterable != undefined) forOf(iterable, IS_MAP, that[ADDER], that);
return that;
});
C.prototype = proto;
proto.constructor = C;
}
if (THROWS_ON_PRIMITIVES || BUGGY_ZERO) {
fixMethod('delete');
fixMethod('has');
IS_MAP && fixMethod('get');
}
if (BUGGY_ZERO || HASNT_CHAINING) fixMethod(ADDER);
// weak collections should not contains .clear method
if (IS_WEAK && proto.clear) delete proto.clear;
}
setToStringTag(C, NAME);
O[NAME] = C;
$export($export.G + $export.W + $export.F * (C != Base), O);
if (!IS_WEAK) common.setStrong(C, NAME, IS_MAP);
return C;
};

Some files were not shown because too many files have changed in this diff Show More