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
@@ -112,14 +112,14 @@ export default class ActionSnippet extends React.PureComponent<Props> {
if (item !== null) {
const infusion = ItemUtils.getInfusion(item);
name = (
<React.Fragment>
<>
<ItemName item={item} />
{infusion !== null && (
<span className="ct-feature-snippet__heading-extra">
(Infusion: {InfusionUtils.getName(infusion)})
</span>
)}
</React.Fragment>
</>
);
}
}
@@ -1,5 +1,7 @@
import clsx from "clsx";
import { orderBy } from "lodash";
import React, { HTMLAttributes, useMemo } from "react";
import { AttackTable } from "@dndbeyond/character-components/es";
import {
AbilityLookup,
@@ -21,15 +23,15 @@ import {
CharacterTheme,
Action,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import ActionSnippet from "../ActionSnippet";
import BasicActions from "../BasicActions";
import { FeatureSnippetSpells } from "../FeatureSnippet";
import clsx from "clsx";
import styles from "./styles.module.css";
import { ActionListSection } from "./ActionListSection";
import styles from "./styles.module.css";
interface ActionsListProps extends HTMLAttributes<HTMLDivElement> {
heading: React.ReactNode;
@@ -49,10 +51,10 @@ interface ActionsListProps extends HTMLAttributes<HTMLDivElement> {
abilityLookup: AbilityLookup;
dataOriginRefData: DataOriginRefData;
proficiencyBonus: number;
rollContext: IRollContext;
rollContext: RollContext;
inventoryLookup: InventoryLookup;
snippetData: SnippetData;
// ActionHandlers
onAttackClick: (attack: Attack) => void;
onActionClick: (action: Action) => void;
@@ -60,7 +62,7 @@ interface ActionsListProps extends HTMLAttributes<HTMLDivElement> {
onActionUseSet: (action: Action, used: number) => void;
onSpellClick: (spell: Spell) => void;
onSpellUseSet: (spell: Spell, used: number) => void;
};
}
export const ActionsList = ({
heading,
@@ -94,16 +96,17 @@ export const ActionsList = ({
className,
...props
}: ActionsListProps) => {
const { actionLookup } = useCharacterEngine();
const { actionLookup, itemPlans } = useCharacterEngine();
const isVisible = useMemo(
() => !!basicActions.length
|| !!actions.length
|| !!ritualSpells.length
|| !!attacks.length,
() =>
!!basicActions.length ||
!!actions.length ||
!!ritualSpells.length ||
!!attacks.length,
[basicActions, actions, ritualSpells, attacks]
);
const attackTableProps = {
attacks,
weaponSpellDamageGroups,
@@ -117,6 +120,7 @@ export const ActionsList = ({
dataOriginRefData,
proficiencyBonus,
rollContext,
itemPlans,
};
const spellSnippetProps = {
@@ -146,43 +150,42 @@ export const ActionsList = ({
}, [actions]);
const orderedRitualSpells = useMemo(
() => orderBy(
ritualSpells,
(spell) => SpellUtils.getName(spell)
),
() => orderBy(ritualSpells, (spell) => SpellUtils.getName(spell)),
[ritualSpells]
);
const features = useMemo(() => actions.filter(
a => a.type === Constants.ActivatableTypeEnum.ACTION)
.map(feature => {
const action = HelperUtils.lookupDataOrFallback(
actionLookup,
ActionUtils.getUniqueKey(feature.entity)
);
return (
<div
className={styles.activatable}
key={feature.key}
data-testid="actions-list-activatable-feature"
>
<ActionSnippet
theme={theme}
action={action ?? feature.entity}
onActionClick={onActionClick}
onActionUseSet={onActionUseSet}
abilityLookup={abilityLookup}
inventoryLookup={inventoryLookup}
ruleData={ruleData}
snippetData={snippetData}
isInteractive={isInteractive}
showActivationInfo={showActivationInfo}
activation={feature.activation}
proficiencyBonus={proficiencyBonus}
/>
</div>
);
}),
const features = useMemo(
() =>
actions
.filter((a) => a.type === Constants.ActivatableTypeEnum.ACTION)
.map((feature) => {
const action = HelperUtils.lookupDataOrFallback(
actionLookup,
ActionUtils.getUniqueKey(feature.entity)
);
return (
<div
className={styles.activatable}
key={feature.key}
data-testid="actions-list-activatable-feature"
>
<ActionSnippet
theme={theme}
action={action ?? feature.entity}
onActionClick={onActionClick}
onActionUseSet={onActionUseSet}
abilityLookup={abilityLookup}
inventoryLookup={inventoryLookup}
ruleData={ruleData}
snippetData={snippetData}
isInteractive={isInteractive}
showActivationInfo={showActivationInfo}
activation={feature.activation}
proficiencyBonus={proficiencyBonus}
/>
</div>
);
}),
[
actions,
actionLookup,
@@ -196,7 +199,8 @@ export const ActionsList = ({
isInteractive,
showActivationInfo,
proficiencyBonus,
]);
]
);
return isVisible ? (
<div className={clsx([styles.actionsList, className])} {...props}>
@@ -207,10 +211,16 @@ export const ActionsList = ({
{heading}
</div>
<div>
{!!attacks.length && <AttackTable className={styles.attackTable} {...attackTableProps} />}
{!!attacks.length && (
<AttackTable className={styles.attackTable} {...attackTableProps} />
)}
{!!basicActions.length && (
<ActionListSection headingText="Actions in Combat" testId="basic" theme={theme}>
<ActionListSection
headingText="Actions in Combat"
testId="basic"
theme={theme}
>
<BasicActions
onActionClick={onBasicActionClick}
basicActions={basicActions}
@@ -221,13 +231,21 @@ export const ActionsList = ({
{!!spells.length && (
<ActionListSection headingText="Spells" theme={theme}>
<FeatureSnippetSpells layoutType={"compact"} spells={spells} {...spellSnippetProps} />
<FeatureSnippetSpells
layoutType={"compact"}
spells={spells}
{...spellSnippetProps}
/>
</ActionListSection>
)}
{!!ritualSpells.length && (
<ActionListSection headingText="Ritual Spells" theme={theme}>
<FeatureSnippetSpells layoutType={"compact"} spells={orderedRitualSpells} {...spellSnippetProps} />
<FeatureSnippetSpells
layoutType={"compact"}
spells={orderedRitualSpells}
{...spellSnippetProps}
/>
</ActionListSection>
)}
@@ -1,110 +0,0 @@
import React, { useContext, useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
FeatureManager,
FeaturesManager,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { PaneIdentifierUtils } from "~/tools/js/Shared/utils";
import { Snippet } from "~/tools/js/smartComponents";
import { CharacterFeaturesManagerContext } from "../../../Shared/managers/CharacterFeaturesManagerContext";
interface Props {
// onActionUseSet: (action: Action, uses: number) => void;
// onActionClick: (action: Action) => void;
// onSpellClick: (spell: Spell) => void;
// onSpellUseSet: (spell: Spell, uses: number) => void;
// onFeatureClick: (feat: FeatManager) => void;
// snippetData: SnippetData;
// ruleData: RuleData;
// abilityLookup: AbilityLookup;
// dataOriginRefData: DataOriginRefData;
// isReadonly: boolean;
// proficiencyBonus: number;
// theme: CharacterTheme;
}
export const BlessingsDetail: React.FC<Props> = () => {
const { characterFeaturesManager } = useContext(
CharacterFeaturesManagerContext
);
const {
pane: { paneHistoryStart },
} = useSidebar();
const [blessings, setBlessings] = useState<Array<FeatureManager>>([]);
useEffect(() => {
async function onUpdate() {
const blessings = await characterFeaturesManager.getBlessings();
setBlessings(blessings);
}
return FeaturesManager.subscribeToUpdates({ onUpdate });
}, [setBlessings]);
const snippetData = useSelector(rulesEngineSelectors.getSnippetData);
// const ruleData = useSelector(rulesEngineSelectors.getRuleData);
// const abilityLookup = useSelector(rulesEngineSelectors.getAbilityLookup);
// const dataOriginRefData = useSelector(rulesEngineSelectors.getDataOriginRefData);
// const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const proficiencyBonus = useSelector(
rulesEngineSelectors.getProficiencyBonus
);
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
return (
<div className="ct-blessings-detail">
{blessings.length ? (
<React.Fragment>
{blessings.map((blessing) => (
<div
onClick={(evt: React.MouseEvent) => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(
PaneComponentEnum.BLESSING_DETAIL,
PaneIdentifierUtils.generateBlessing(blessing.getId())
);
}}
className={"ct-feature-snippet"}
key={blessing.getId()}
>
<div
className={`ct-feature-snippet__heading ${
theme?.isDarkMode
? "ct-feature-snippet__heading--dark-mode"
: ""
}`}
>
{blessing.getName()}
</div>
<div className="ct-feature-snippet__content">
<Snippet
snippetData={snippetData}
proficiencyBonus={proficiencyBonus}
theme={theme}
>
{blessing.getDescription()}
</Snippet>
</div>
</div>
))}
</React.Fragment>
) : (
<div
className={`ct-blessings-detail__default ${
theme.isDarkMode ? "ct-blessings-detail__default--dark-mode" : ""
}`}
>
<p>There is nothing here.</p>
</div>
)}
</div>
);
};
export default BlessingsDetail;
@@ -1,4 +0,0 @@
import BlessingsDetail from "./BlessingsDetail";
export default BlessingsDetail;
export { BlessingsDetail };
@@ -1,203 +0,0 @@
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import CardActionArea from "@mui/material/CardActionArea";
import CardActions from "@mui/material/CardActions";
import Typography from "@mui/material/Typography";
import { useState } from "react";
import { useSelector } from "react-redux";
import { MaxCharactersDialog } from "~/components/MaxCharactersDialog";
import { useAuth } from "~/contexts/Authentication";
import { useClaimCharacter } from "~/hooks/useClaimCharacter";
import backgroundImage from "~/images/claim.png";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import {
DESKTOP_COMPONENT_START_WIDTH,
TABLET_COMPONENT_START_WIDTH,
} from "../../config";
import ClaimConfirmationDialog from "../WatchTourDialog/ClaimConfirmationDialog";
const renderButtonContent = (signedIn: boolean) => (
<Box sx={{ display: "flex", justifyContent: "center", marginLeft: "10px" }}>
{!signedIn && (
<Typography
component="span"
sx={{
display: "flex",
alignItems: "center",
marginRight: "5px",
textTransform: "none",
fontWeight: 400,
fontSize: "14px",
lineHeight: "16px",
}}
>
Create Account to
</Typography>
)}
<Typography
component="span"
sx={{
display: "flex",
alignItems: "center",
textTransform: "none",
fontWeight: 700,
fontSize: "14px",
lineHeight: "16px",
}}
>
Claim Character
</Typography>
<ChevronRightIcon />
</Box>
);
// z-index of the sidebar controls
const zIndex = 60002;
const createAccountLink = `/create-account?returnUrl=${window.location.pathname}`;
const ClaimPremadeButton: React.FC = () => {
const params = new URLSearchParams(globalThis.location.search);
const campaignJoinCode = params.get("campaignJoinCode");
const isAssigned = params.get("isAssigned") === "false" ? false : true;
const [isMaxCharacterMessageOpen, setIsMaxCharacterMessageOpen] =
useState(false);
const envDimensions = useSelector(appEnvSelectors.getDimensions);
const { characterSlotLimit, activeCharacterCount } = useSelector(
appEnvSelectors.getCharacterSlots
);
// Character slot limit is null for admin accounts
const hasOpenSlot =
characterSlotLimit === null || activeCharacterCount < characterSlotLimit;
const [
claimCharacter,
isClaimingCharacter,
isFinishedClaimingCharacter,
newCharacterId,
campaignId,
] = useClaimCharacter({
campaignJoinCode,
isAssigned,
});
const user = useAuth();
const signedIn = !!user;
const handleClaimButtonClick = () => {
hasOpenSlot ? claimCharacter() : setIsMaxCharacterMessageOpen(true);
};
return (
<>
{envDimensions.window.width >= TABLET_COMPONENT_START_WIDTH ? (
<Card
className="claimPremade-button"
hidden={isFinishedClaimingCharacter && !!newCharacterId}
sx={{
zIndex,
position: "fixed",
right: 30,
bottom:
envDimensions.window.width >= DESKTOP_COMPONENT_START_WIDTH
? 30
: 70,
width: 300,
background: "#551710",
borderRadius: "8px",
overflow: "visible",
boxShadow:
"0px 57px 23px rgba(0, 0, 0, 0.01), 0px 32px 19px rgba(0, 0, 0, 0.05), 0px 14px 14px rgba(0, 0, 0, 0.08), 0px 4px 8px rgba(0, 0, 0, 0.09), 0px 0px 0px rgba(0, 0, 0, 0.09)",
"&:hover": {
"& .claimPremade-buttonContent": {
background: "#F5F3EE",
boxShadow: "0px -4px 12px rgba(0, 0, 0, 0.25)",
},
},
}}
>
<CardActionArea
sx={{ textAlign: "center" }}
{...(!signedIn && { href: createAccountLink })}
onClick={signedIn ? handleClaimButtonClick : undefined}
disabled={isClaimingCharacter}
>
<Box
component="img"
src={backgroundImage}
sx={{ position: "relative", top: -20, width: 244, height: 144 }}
/>
<CardActions
className="claimPremade-buttonContent"
sx={{
justifyContent: "center",
height: 48,
color: "#000000",
background: "#FFFFFF",
borderRadius: "0 0 8px 8px",
}}
>
{renderButtonContent(signedIn)}
</CardActions>
</CardActionArea>
</Card>
) : (
<Button
variant="contained"
className="claimPremade-button"
{...(!signedIn && { href: createAccountLink })}
sx={{
display:
isFinishedClaimingCharacter && !!newCharacterId
? "none"
: "inline-flex",
zIndex,
position: "fixed",
left: "50%",
bottom: 70,
width: 300,
height: "50px",
marginLeft: "-150px",
color: "#000000",
backgroundColor: "#FFFFFF",
boxShadow:
"0px 57px 23px rgba(0, 0, 0, 0.01), 0px 32px 19px rgba(0, 0, 0, 0.05), 0px 14px 14px rgba(0, 0, 0, 0.08), 0px 4px 8px rgba(0, 0, 0, 0.09), 0px 0px 0px rgba(0, 0, 0, 0.09)",
"&:hover": {
backgroundColor: "#F5F3EE",
boxShadow:
"0px 57px 23px rgba(0, 0, 0, 0.01), 0px 32px 19px rgba(0, 0, 0, 0.05), 0px 14px 14px rgba(0, 0, 0, 0.08), 0px 4px 8px rgba(0, 0, 0, 0.09), 0px 0px 0px rgba(0, 0, 0, 0.09)",
},
"&.Mui-disabled": {
backgroundColor: "#FFFFFF",
},
}}
onClick={signedIn ? handleClaimButtonClick : undefined}
disabled={isClaimingCharacter}
>
{renderButtonContent(signedIn)}
</Button>
)}
<MaxCharactersDialog
open={isMaxCharacterMessageOpen}
onClose={() => setIsMaxCharacterMessageOpen(false)}
useMyCharactersLink
/>
{isFinishedClaimingCharacter && !!newCharacterId && (
<ClaimConfirmationDialog
characterId={newCharacterId}
campaignId={campaignId}
/>
)}
</>
);
};
export default ClaimPremadeButton;
@@ -1,4 +0,0 @@
import ClaimPremadeButton from "./ClaimPremadeButton";
export default ClaimPremadeButton;
export { ClaimPremadeButton as ClaimPremadeDialog };
@@ -16,6 +16,7 @@ interface CarouselWindowItem {
offset: number;
idx: number;
key: string;
itemKey: string;
}
interface Props {
@@ -233,7 +234,7 @@ export default class ComponentCarousel extends React.PureComponent<
return posIndex % total;
} else {
const indexMod = Math.abs(posIndex) % total;
return (indexMod === 0 ? 0 : 1) * (total - indexMod);
return indexMod === 0 ? 0 : total - indexMod;
}
};
@@ -306,6 +307,7 @@ export default class ComponentCarousel extends React.PureComponent<
swipedAmount,
childrenCount,
currentIdx,
childByIdxLookup,
} = state;
let currentPosIdx: number = ComponentCarousel.getSwipedItemPosIdx(
@@ -324,14 +326,21 @@ export default class ComponentCarousel extends React.PureComponent<
i,
childrenCount
);
const child = childByIdxLookup[itemIdx];
const itemKey = child?.props?.itemKey;
// Find existing window item by matching both itemKey AND offset
let existingItem = windowItems.find(
(windowItem) => windowItem.idx === itemIdx
(windowItem) =>
windowItem.itemKey === itemKey && windowItem.offset === offset
);
newWindowItems.push({
offset,
idx: itemIdx,
key: existingItem ? existingItem.key : uniqueId(),
itemKey: itemKey,
// Use itemKey + position index i to ensure uniqueness
key: existingItem ? existingItem.key : `${itemKey}-${i}`,
});
offset++;
@@ -505,7 +514,7 @@ export default class ComponentCarousel extends React.PureComponent<
<div className={classNames.join(" ")} ref={this.placeholderRef}>
<TransitionMotion styles={placeholderStyles}>
{(interpolatedStyles) => (
<React.Fragment>
<>
{interpolatedStyles.map((config) => {
const { PlaceholderComponent, placeholderProps, itemKey } =
config.data.child.props;
@@ -526,12 +535,11 @@ export default class ComponentCarousel extends React.PureComponent<
</div>
);
})}
</React.Fragment>
</>
)}
</TransitionMotion>
</div>
);
//`
};
renderActiveComponent = (): React.ReactNode => {
@@ -27,7 +27,7 @@ export default class ConditionsSummary extends React.PureComponent<Props> {
const { conditions, theme } = this.props;
return (
<React.Fragment>
<>
{conditions.map((condition) => (
<ConditionName
theme={theme}
@@ -35,7 +35,7 @@ export default class ConditionsSummary extends React.PureComponent<Props> {
condition={condition}
/>
))}
</React.Fragment>
</>
);
};
@@ -1,6 +1,6 @@
import React from "react";
import React, { PropsWithChildren } from "react";
interface Props {
interface Props extends PropsWithChildren {
header: React.ReactNode;
extra?: React.ReactNode;
}
@@ -57,14 +57,8 @@ const CoinContent = ({
}
const { pp, gp, ep, sp, cp } = coin;
const shouldReadonly =
isReadonly ||
(containerDefinitionKey ===
coinManager.getPartyEquipmentContainerDefinitionKey() &&
coinManager.isSharingTurnedDeleteOnly());
return (
<React.Fragment>
<>
<div className="ct-currency-pane__currencies">
<CurrencyPaneCurrencyRow
name="Platinum (pp)"
@@ -77,7 +71,7 @@ const CoinContent = ({
value={pp}
coinType={Constants.CoinTypeEnum.pp}
conversion="1 pp = 10 gp"
isReadonly={shouldReadonly}
isReadonly={isReadonly}
/>
<CurrencyPaneCurrencyRow
name="Gold (gp)"
@@ -87,7 +81,7 @@ const CoinContent = ({
onError={(errorType) => handleCurrencyChangeError("Gold", errorType)}
value={gp}
coinType={Constants.CoinTypeEnum.gp}
isReadonly={shouldReadonly}
isReadonly={isReadonly}
/>
<CurrencyPaneCurrencyRow
name="Electrum (ep)"
@@ -100,7 +94,7 @@ const CoinContent = ({
value={ep}
conversion="1 gp = 2 ep"
coinType={Constants.CoinTypeEnum.ep}
isReadonly={shouldReadonly}
isReadonly={isReadonly}
/>
<CurrencyPaneCurrencyRow
name="Silver (sp)"
@@ -113,7 +107,7 @@ const CoinContent = ({
value={sp}
conversion="1 gp = 10 sp"
coinType={Constants.CoinTypeEnum.sp}
isReadonly={shouldReadonly}
isReadonly={isReadonly}
/>
<CurrencyPaneCurrencyRow
name="Copper (cp)"
@@ -125,21 +119,16 @@ const CoinContent = ({
}
value={cp}
conversion="1 gp = 100 cp"
isReadonly={shouldReadonly}
isReadonly={isReadonly}
coinType={Constants.CoinTypeEnum.cp}
/>
</div>
{(coinManager.isSharingTurnedOn() ||
!coinManager.isSharedContainerDefinitionKey(
containerDefinitionKey
)) && (
<CurrencyPaneAdjuster
onAdjust={handleCurrencyAdjust}
isReadonly={isReadonly}
containerDefinitionKey={containerDefinitionKey}
/>
)}
</React.Fragment>
<CurrencyPaneAdjuster
onAdjust={handleCurrencyAdjust}
isReadonly={isReadonly}
containerDefinitionKey={containerDefinitionKey}
/>
</>
);
};
@@ -43,7 +43,7 @@ export default class DefensesSummary extends React.PureComponent<Props> {
const { theme } = this.props;
let orderedDefenses = orderBy(defenses, (group) => group.name);
return (
<React.Fragment>
<>
{orderedDefenses.map((defense: DefenseAdjustmentGroup, idx) => (
<span
className={`ct-defenses-summary__defense ${
@@ -61,7 +61,7 @@ export default class DefensesSummary extends React.PureComponent<Props> {
</Tooltip>
</span>
))}
</React.Fragment>
</>
);
//`
};
@@ -133,7 +133,7 @@ export default class DefensesSummary extends React.PureComponent<Props> {
vulnerabilities.length > 0 &&
immunities.length > 0;
return (
<React.Fragment>
<>
{this.renderDamageAdjustmentGroup(
DEFENSE_GROUP.RESISTANCE,
"Resistances",
@@ -155,7 +155,7 @@ export default class DefensesSummary extends React.PureComponent<Props> {
isSingleLine,
theme
)}
</React.Fragment>
</>
);
};
@@ -1,195 +0,0 @@
import React from "react";
import { Tooltip } from "@dndbeyond/character-common-components/es";
import {
CharacterCurrencyContract,
Constants,
FormatUtils,
RuleDataUtils,
} from "@dndbeyond/character-rules-engine/es";
import { NumberDisplay } from "~/components/NumberDisplay";
import CurrencyButton from "../CurrencyButton";
interface Props {
currencies: CharacterCurrencyContract | null;
weight: number;
weightSpeedType: Constants.WeightSpeedTypeEnum;
onCurrencyClick?: (evt: React.MouseEvent | React.KeyboardEvent) => void;
onWeightClick?: () => void;
onCampaignClick?: () => void;
onManageClick?: () => void;
enableManage: boolean;
isReadonly: boolean;
isDarkMode: boolean;
shouldShowPartyInventory: boolean;
campaignName: string | null;
}
export default class EquipmentOverview extends React.PureComponent<Props> {
static defaultProps = {
enableManage: true,
isReadonly: false,
};
handleCurrencyClick = (evt: React.MouseEvent | React.KeyboardEvent): void => {
const { onCurrencyClick } = this.props;
if (onCurrencyClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onCurrencyClick(evt);
}
};
handleCampaignClick = (evt: React.MouseEvent | React.KeyboardEvent): void => {
const { onCampaignClick } = this.props;
if (onCampaignClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onCampaignClick();
}
};
handleWeightClick = (evt: React.MouseEvent | React.KeyboardEvent): void => {
const { onWeightClick } = this.props;
if (onWeightClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onWeightClick();
}
};
handleManageClick = (evt: React.MouseEvent | React.KeyboardEvent): void => {
const { onManageClick } = this.props;
if (onManageClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onManageClick();
}
};
render() {
const {
weight,
weightSpeedType,
enableManage,
isReadonly,
isDarkMode,
shouldShowPartyInventory,
campaignName,
currencies,
} = this.props;
let weightSpeedLabel =
RuleDataUtils.getWeightSpeedTypeLabel(weightSpeedType);
return (
// TODO: add a min height?
<div className="ct-equipment-overview">
{!shouldShowPartyInventory ? (
<div
role="button"
className="ct-equipment-overview__weight"
onClick={this.handleWeightClick}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
this.handleWeightClick(evt);
}
}}
>
<div className="ct-equipment-overview__weight-carried">
<span
className={`ct-equipment-overview__weight-carried-label ${
isDarkMode
? "ct-equipment-overview__weight-carried--dark-mode"
: ""
}`}
>
Weight Carried:
</span>
<span
className={`ct-equipment-overview__weight-carried-amount ${
isDarkMode
? "ct-equipment-overview__weight-carried--dark-mode"
: ""
}`}
>
<NumberDisplay
data-testid="weight-carried-number"
type="weightInLb"
number={weight}
/>
</span>
</div>
<div
className={`ct-equipment-overview__weight-speed ct-equipment-overview__weight-speed--${FormatUtils.slugify(
weightSpeedLabel
)}`}
>
{weightSpeedLabel}
</div>
</div>
) : (
<div
role="button"
className="ct-equipment-overview__weight"
onClick={this.handleCampaignClick}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
this.handleCampaignClick(evt);
}
}}
>
<div className="ct-equipment-overview__weight-carried">
<span
className={`ct-equipment-overview__weight-carried-label ${
isDarkMode
? "ct-equipment-overview__weight-carried--dark-mode"
: ""
}`}
>
CAMPAIGN:
<span
className={`ct-equipment-overview__weight-carried-amount ${
isDarkMode
? "ct-equipment-overview__weight-carried--dark-mode"
: ""
}`}
>
{campaignName || "N/A"}
</span>
</span>
</div>
</div>
)}
<div className="ct-equipment-overview__sep" />
<CurrencyButton
handleCurrencyClick={this.handleCurrencyClick}
coin={currencies}
isDarkMode={isDarkMode}
shouldShowPartyInventory={shouldShowPartyInventory}
/>
{enableManage && !isReadonly && (
<div
role="button"
className="ct-equipment-overview__manage"
onClick={this.handleManageClick}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
this.handleManageClick(evt);
}
}}
>
<Tooltip title="Manage Equipment" isDarkMode={isDarkMode}>
<span className="ct-equipment-overview__manage-icon" />
</Tooltip>
</div>
)}
</div>
);
}
}
@@ -1,4 +0,0 @@
import EquipmentOverview from "./EquipmentOverview";
export default EquipmentOverview;
export { EquipmentOverview };
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class ExtrasFilterAdvancedFilter extends React.PureComponent {
export default class ExtrasFilterAdvancedFilter extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-extras-filter__adv-filter">{this.props.children}</div>
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class ExtrasFilterAdvancedFilterLabel extends React.PureComponent {
export default class ExtrasFilterAdvancedFilterLabel extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-extras-filter__adv-filter-label">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class ExtrasFilterAdvancedFilterOption extends React.PureComponent {
export default class ExtrasFilterAdvancedFilterOption extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-extras-filter__adv-filter-option">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class ExtrasFilterAdvancedFilterOptions extends React.PureComponent {
export default class ExtrasFilterAdvancedFilterOptions extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-extras-filter__adv-filter-options">
@@ -50,7 +50,7 @@ export const FeatsDetail: React.FC<Props> = ({
return (
<div className="ct-feats-detail">
{currentFeats.length ? (
<React.Fragment>
<>
{currentFeats.map((feat) => (
<FeatFeatureSnippet
key={feat.getId()}
@@ -69,7 +69,7 @@ export const FeatsDetail: React.FC<Props> = ({
theme={theme}
/>
))}
</React.Fragment>
</>
) : (
<div
className={`ct-feats-detail__default ${
@@ -73,6 +73,7 @@ export default class FeatureSnippetAction extends React.PureComponent<Props> {
case Constants.ActivationTypeEnum.ACTION:
case Constants.ActivationTypeEnum.REACTION:
case Constants.ActivationTypeEnum.BONUS_ACTION:
case Constants.ActivationTypeEnum.SPECIAL:
activationDisplay = ActivationUtils.renderActivation(
activation,
ruleData
@@ -5,6 +5,7 @@ import {
BaseFeat,
CharacterTheme,
Choice,
ChoiceUtils,
ClassDefinitionContract,
Constants,
DataOriginBaseAction,
@@ -112,7 +113,11 @@ export default class FeatureSnippetChoices extends React.PureComponent<Props> {
abilityLookup={abilityLookup}
dataOriginRefData={dataOriginRefData}
sourceDataLookup={sourceDataLookup}
showDescription={showDescription}
showDescription={
ChoiceUtils.getItemDefinitionKey(choice)
? false
: showDescription
}
isInteractive={isInteractive}
proficiencyBonus={proficiencyBonus}
theme={theme}
@@ -53,11 +53,7 @@ export default class FeatureSnippetInfusionChoices extends React.PureComponent<P
const simulatedInfusion =
KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
if (simulatedInfusion !== null) {
nameNode = (
<React.Fragment>
{InfusionUtils.getName(simulatedInfusion)}
</React.Fragment>
);
nameNode = <>{InfusionUtils.getName(simulatedInfusion)}</>;
onClick = this.handleInfusionChoiceClick.bind(
this,
infusionChoice
@@ -161,7 +161,7 @@ export default class FeatureSnippetLimitedUse extends React.PureComponent<Props>
}
return (
<React.Fragment>
<>
<div
className={`ct-feature-snippet__limited-use ${
theme.isDarkMode ? "ct-feature-snippet__limited-use--dark-mode" : ""
@@ -180,7 +180,7 @@ export default class FeatureSnippetLimitedUse extends React.PureComponent<Props>
{extraNode}
</div>
)}
</React.Fragment>
</>
);
}
}
@@ -160,7 +160,7 @@ export class Infusions extends React.PureComponent<Props, State> {
const { infusionChoices, ruleData, theme } = this.props;
return (
<React.Fragment>
<>
{infusionChoices.map((infusionChoice) => {
const knownInfusion =
InfusionChoiceUtils.getKnownInfusion(infusionChoice);
@@ -279,7 +279,7 @@ export class Infusions extends React.PureComponent<Props, State> {
</div>
);
})}
</React.Fragment>
</>
);
};
@@ -1,4 +1,5 @@
import { FC } from "react";
import { useSearchParams } from "react-router-dom";
import { BuilderLinkButton } from "../../../Shared/components/common/LinkButton";
@@ -11,6 +12,10 @@ export const InvalidCharacter: FC<InvalidCharacterProps> = ({
builderUrl,
isReadonly,
}) => {
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
const sendToNonVttBuilderUrl = builderUrl.replace("view=vtt", "");
return (
<div className="ct-invalid-character">
<h2>Character Not Ready!</h2>
@@ -30,9 +35,10 @@ export const InvalidCharacter: FC<InvalidCharacterProps> = ({
<p>
<BuilderLinkButton
url={builderUrl}
url={isVttView ? sendToNonVttBuilderUrl : builderUrl}
className="ct-invalid-character__button"
size="oversized"
target={isVttView ? "_blank" : undefined}
>
Character Builder
</BuilderLinkButton>
@@ -287,9 +287,11 @@ export default class InventoryFilter extends React.PureComponent<Props, State> {
handleFiltersClear = (): void => {
const inventory = this.getInventory(this.props);
const partyInventory = this.getPartyInventory(this.props);
this.setState(
{
filteredInventory: inventory,
filteredPartyInventory: partyInventory,
filterQuery: "",
filterTypes: [],
filterRarities: [],
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class InventoryFilterAdvancedFilter extends React.PureComponent {
export default class InventoryFilterAdvancedFilter extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-inventory-filter__adv-filter">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class InventoryFilterAdvancedFilterLabel extends React.PureComponent {
export default class InventoryFilterAdvancedFilterLabel extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-inventory-filter__adv-filter-label">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class InventoryFilterAdvancedFilterOption extends React.PureComponent {
export default class InventoryFilterAdvancedFilterOption extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-inventory-filter__adv-filter-option">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class InventoryFilterAdvancedFilterOptions extends React.PureComponent {
export default class InventoryFilterAdvancedFilterOptions extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-inventory-filter__adv-filter-options">
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import {
@@ -8,6 +7,7 @@ import {
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
import { NumberDisplay } from "~/components/NumberDisplay";
import a11yStyles from "~/styles/accessibility.module.css";
interface Props {
proficiencyBonus: number;
@@ -31,7 +31,7 @@ export default class ProficiencyBonusBox extends React.PureComponent<Props> {
return (
<section className="ct-proficiency-bonus-box" onClick={this.handleClick}>
<BoxBackground StyleComponent={BeveledBoxSvg94x89} theme={theme} />
<h2 style={visuallyHidden}>Proficiency Bonus</h2>
<h2 className={a11yStyles.screenreaderOnly}>Proficiency Bonus</h2>
<div
className={`ct-proficiency-bonus-box__heading ${
theme.isDarkMode
@@ -31,7 +31,7 @@ export default class ProficiencyGroups extends React.PureComponent<Props> {
let itemsNode: React.ReactNode = "None";
if (group.modifierGroups.length) {
itemsNode = (
<React.Fragment>
<>
{group.modifierGroups.map((modifierGroup, idx) => {
let title: string = modifierGroup.sources.join(", ");
return (
@@ -59,7 +59,7 @@ export default class ProficiencyGroups extends React.PureComponent<Props> {
</Tooltip>
);
})}
</React.Fragment>
</>
);
}
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import {
@@ -11,6 +10,8 @@ import {
ProficiencyGroup,
} from "@dndbeyond/character-rules-engine/es";
import a11yStyles from "~/styles/accessibility.module.css";
import { StyleSizeTypeEnum } from "../../../Shared/reducers/appEnv";
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
import ProficiencyGroups from "../ProficiencyGroups";
@@ -36,7 +37,9 @@ export default class ProficiencyGroupsBox extends React.PureComponent<Props> {
return (
<section className="ct-proficiency-groups-box">
<BoxBackground StyleComponent={BoxBackgroundComponent} theme={theme} />
<h2 style={visuallyHidden}>Proficiencies and Languages</h2>
<h2 className={a11yStyles.screenreaderOnly}>
Proficiencies and Languages
</h2>
<ProficiencyGroups
proficiencyGroups={proficiencyGroups}
onClick={onClick}
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import { orderBy } from "lodash";
import { Tooltip } from "@dndbeyond/character-common-components/es";
@@ -18,7 +17,9 @@ import {
RuleData,
SituationalSavingThrowInfoLookup,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import a11yStyles from "~/styles/accessibility.module.css";
import DiceAdjustmentSummary from "../../../Shared/components/DiceAdjustmentSummary";
import { StyleSizeTypeEnum } from "../../../Shared/reducers/appEnv";
@@ -37,7 +38,7 @@ interface Props {
dimensions: AppEnvDimensionsState;
theme: CharacterTheme;
diceEnabled?: boolean;
rollContext: IRollContext;
rollContext: RollContext;
}
const sortDiceAdjustments = (
@@ -276,7 +277,7 @@ export default function SavingThrowsBox({
return (
<section className="ct-saving-throws-box" onClick={handleClick}>
<BoxBackground StyleComponent={BoxBackgroundComponent} theme={theme} />
<h2 style={visuallyHidden}>Saving Throws</h2>
<h2 className={a11yStyles.screenreaderOnly}>Saving Throws</h2>
<div className="ct-saving-throws-box__abilities">
<SavingThrowsSummary
abilities={abilities}
@@ -1,9 +1,11 @@
import clsx from "clsx";
import React from "react";
import {
BoxBackground,
ThemedSenseRowBoxSvg,
ThemedSenseRowSmallBoxSvg,
ThemedSenseRowMinimalSvg,
} from "@dndbeyond/character-components/es";
import {
CharacterTheme,
@@ -14,13 +16,14 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { TypeScriptUtils } from "../../../Shared/utils";
import styles from "./styles.module.css";
interface Props {
passivePerception: number;
passiveInvestigation: number;
passiveInsight: number;
senses: SenseInfo;
rowStyle: "small" | "normal";
rowStyle: "small" | "normal" | "minimal";
onClick?: () => void;
theme: CharacterTheme;
}
@@ -53,8 +56,6 @@ export default class Senses extends React.PureComponent<Props> {
};
renderSummaryInfo = (): React.ReactNode => {
const { theme } = this.props;
let senseKeys: Array<Constants.SenseTypeEnum> = [
Constants.SenseTypeEnum.BLINDSIGHT,
Constants.SenseTypeEnum.DARKVISION,
@@ -66,24 +67,17 @@ export default class Senses extends React.PureComponent<Props> {
.map((senseKey) => this.getSenseSummary(senseKey))
.filter(TypeScriptUtils.isNotNullOrUndefined);
let summaryClasses: Array<string> = ["ct-senses__summary"];
if (!senseSummaries.length) {
summaryClasses.push("ct-senses__summary--empty");
}
if (theme?.isDarkMode) {
summaryClasses.push("ct-senses__summary--dark-mode");
}
if (senseSummaries.length) {
return (
<div className={summaryClasses.join(" ")}>
{senseSummaries.join(", ")}
</div>
);
}
return (
<div className={summaryClasses.join(" ")}>Additional Sense Types</div>
<div
className={clsx([
styles.summary,
!senseSummaries.length && styles.summaryEmpty,
])}
>
{!senseSummaries.length
? "Additional Sense Types"
: senseSummaries.join(", ")}
</div>
);
};
@@ -96,84 +90,140 @@ export default class Senses extends React.PureComponent<Props> {
theme,
} = this.props;
let isMinimal = false;
let StyleComponent: React.ComponentType<any> = ThemedSenseRowBoxSvg;
if (rowStyle === "small") {
StyleComponent = ThemedSenseRowSmallBoxSvg;
} else if (rowStyle === "minimal") {
StyleComponent = ThemedSenseRowMinimalSvg;
isMinimal = true;
}
return (
<div className="ct-senses" onClick={this.handleClick}>
<div className="ct-senses__callouts">
<div className="ct-senses__callout">
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div
className={`ct-senses__callout-value ${
theme?.isDarkMode ? "ct-senses__callout-value--dark-mode" : ""
}`}
>
{passivePerception}
<div className={styles.senses} onClick={this.handleClick}>
{isMinimal ? (
<div className={styles.minimalRow}>
<div className={styles.minimalItem}>
<div className={styles.minimalCallout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>{passivePerception}</div>
</div>
<div
className={clsx([
styles.calloutLabel,
styles.minimalCalloutLabel,
])}
>
Perception
</div>
</div>
<div
className={`ct-senses__callout-label ${
theme?.isDarkMode ? "ct-senses__callout-label--dark-mode" : ""
}`}
>
Passive Perception
<div className={styles.minimalItem}>
<div className={styles.minimalCallout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>
{passiveInvestigation}
</div>
</div>
<div
className={clsx([
styles.calloutLabel,
styles.minimalCalloutLabel,
])}
>
Investigation
</div>
</div>
<div className={styles.minimalItem}>
<div className={styles.minimalCallout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>{passiveInsight}</div>
</div>
<div
className={clsx([
styles.calloutLabel,
styles.minimalCalloutLabel,
])}
>
Insight
</div>
</div>
</div>
<div className="ct-senses__callout">
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div
className={`ct-senses__callout-value ${
theme?.isDarkMode ? "ct-senses__callout-value--dark-mode" : ""
}`}
>
{passiveInvestigation}
) : (
<div>
<div className={styles.callout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>{passivePerception}</div>
<div
className={clsx([
styles.calloutLabel,
styles.regularCalloutLabel,
])}
>
Passive Perception
</div>
</div>
<div
className={`ct-senses__callout-label ${
theme?.isDarkMode ? "ct-senses__callout-label--dark-mode" : ""
}`}
>
Passive Investigation
<div className={styles.callout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>{passiveInvestigation}</div>
<div
className={clsx([
styles.calloutLabel,
styles.regularCalloutLabel,
])}
>
Passive Investigation
</div>
</div>
<div className={styles.callout}>
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div className={styles.calloutValue}>{passiveInsight}</div>
<div
className={clsx([
styles.calloutLabel,
styles.regularCalloutLabel,
])}
>
Passive Insight
</div>
</div>
</div>
<div className="ct-senses__callout">
<BoxBackground
StyleComponent={StyleComponent}
theme={{
...theme,
themeColor: `${theme?.themeColor}80`,
}}
/>
<div
className={`ct-senses__callout-value ${
theme?.isDarkMode ? "ct-senses__callout-value--dark-mode" : ""
}`}
>
{passiveInsight}
</div>
<div
className={`ct-senses__callout-label ${
theme?.isDarkMode ? "ct-senses__callout-label--dark-mode" : ""
}`}
>
Passive Insight
</div>
</div>
</div>
)}
{this.renderSummaryInfo()}
</div>
);
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import {
@@ -11,6 +10,8 @@ import {
SenseInfo,
} from "@dndbeyond/character-rules-engine/es";
import a11yStyles from "~/styles/accessibility.module.css";
import { StyleSizeTypeEnum } from "../../../Shared/reducers/appEnv";
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
import Senses from "../Senses";
@@ -49,7 +50,7 @@ export default class SensesBox extends React.PureComponent<Props> {
return (
<section className="ct-senses-box">
<BoxBackground StyleComponent={BoxBackgroundComponent} theme={theme} />
<h2 style={visuallyHidden}>Senses</h2>
<h2 className={a11yStyles.screenreaderOnly}>Senses</h2>
<Senses
senses={senses}
passiveInsight={passiveInsight}
@@ -1,31 +0,0 @@
import SettingsIcon from "@mui/icons-material/Settings";
import Button from "@mui/material/Button";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
interface Props {
context: string;
isReadonly: boolean;
}
const SettingsButton: React.FC<Props> = ({ context, isReadonly }) => {
const {
pane: { paneHistoryPush },
} = useSidebar();
return !isReadonly ? (
<Button
onClick={(evt) => {
paneHistoryPush(PaneComponentEnum.SETTINGS, {
context,
});
}}
variant="text"
startIcon={<SettingsIcon />}
>
Settings
</Button>
) : null;
};
export default SettingsButton;
@@ -1,4 +0,0 @@
import SettingsButton from "./SettingsButton";
export default SettingsButton;
export { SettingsButton };
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import { orderBy } from "lodash";
import React from "react";
@@ -19,9 +18,12 @@ import {
ValueLookup,
ValueUtils,
} from "@dndbeyond/character-rules-engine/es";
import { RollType, DiceTools, IRollContext } from "@dndbeyond/dice";
import { GameLogContext } from "@dndbeyond/game-log-components";
import { RollTypes } from "@dndbeyond/pocket-dimension-dice/constants";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import { NumberDisplay } from "~/components/NumberDisplay";
import a11yStyles from "~/styles/accessibility.module.css";
interface Props {
skills: Array<Skill>;
@@ -33,7 +35,7 @@ interface Props {
diceEnabled: boolean;
theme: CharacterTheme;
ruleData: RuleData;
rollContext: IRollContext;
rollContext: RollContext;
}
class Skills extends React.PureComponent<Props> {
static defaultProps = {
@@ -87,9 +89,6 @@ class Skills extends React.PureComponent<Props> {
const { customSkills, ruleData, diceEnabled, theme, rollContext } =
this.props;
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
this.context;
return customSkills.map((skill) => {
const modifier = SkillUtils.getModifier(skill);
const statName = RuleDataUtils.getAbilityShortName(
@@ -150,26 +149,17 @@ class Skills extends React.PureComponent<Props> {
"--"
) : (
<DigitalDiceWrapper
diceNotation={DiceTools.CustomD20(
SkillUtils.getModifier(skill)!
)}
rollType={RollType.Check}
rollAction={name ?? "UNKNOWN"}
diceEnabled={diceEnabled}
themeColor={theme.themeColor}
rollContext={rollContext}
rollTargetOptions={
messageTargetOptions
? Object.values(messageTargetOptions?.entities)
: undefined
}
rollTargetDefault={defaultMessageTargetOption}
userId={userId}
diceNotation={`1d20${
modifier > 0 ? `+${modifier}` : modifier !== 0 ? modifier : ""
}`}
rollType={RollTypes.Check}
action={name ?? "UNKNOWN"}
isDiceEnabled={diceEnabled}
entityId={String(rollContext.entityId)}
entityType={String(rollContext.entityType)}
name={String(rollContext.name)}
>
<NumberDisplay
number={modifier}
type="signed"
/>
<NumberDisplay number={modifier} type="signed" />
</DigitalDiceWrapper>
)}
</div>
@@ -189,9 +179,6 @@ class Skills extends React.PureComponent<Props> {
rollContext,
} = this.props;
const [{ messageTargetOptions, defaultMessageTargetOption, userId }] =
this.context;
let orderedSkills = orderBy(skills, (skill) => SkillUtils.getName(skill));
return (
@@ -203,7 +190,7 @@ class Skills extends React.PureComponent<Props> {
>
<div className="ct-skills__header" role="row">
<div className="ct-skills__col--proficiency" role="columnheader">
<span style={visuallyHidden}>Proficiency</span>
<span className={a11yStyles.screenreaderOnly}>Proficiency</span>
<abbr
aria-hidden="true"
title="Proficiency"
@@ -215,7 +202,7 @@ class Skills extends React.PureComponent<Props> {
</abbr>
</div>
<div className="ct-skills__col--stat" role="columnheader">
<span style={visuallyHidden}>Modifier</span>
<span className={a11yStyles.screenreaderOnly}>Modifier</span>
<abbr
aria-hidden="true"
title="Modifier"
@@ -245,7 +232,11 @@ class Skills extends React.PureComponent<Props> {
</span>
</div>
</div>
<div className="ct-skills__list" role="rowgroup">
<div
className="ct-skills__list"
role="rowgroup"
data-scrollable-container="true"
>
{orderedSkills.map((skill) => {
let valueTypes: Array<Constants.AdjustmentTypeEnum> = [
Constants.AdjustmentTypeEnum.SKILL_STAT_OVERRIDE,
@@ -402,27 +393,25 @@ class Skills extends React.PureComponent<Props> {
"--"
) : (
<DigitalDiceWrapper
diceNotation={DiceTools.CustomD20(
SkillUtils.getModifier(skill)!
)}
rollType={RollType.Check}
rollAction={name ? name : "UNKNOWN"}
diceEnabled={diceEnabled}
themeColor={theme.themeColor}
rollContext={rollContext}
rollTargetOptions={
messageTargetOptions
? Object.values(messageTargetOptions?.entities)
: undefined
}
rollTargetDefault={defaultMessageTargetOption}
userId={userId}
diceNotation={`1d20${
modifier > 0
? `+${modifier}`
: modifier !== 0
? modifier
: ""
}`}
rollType={RollTypes.Check}
action={name ? name : "UNKNOWN"}
isDiceEnabled={diceEnabled}
entityId={String(rollContext.entityId)}
entityType={String(rollContext.entityType)}
name={String(rollContext.name)}
>
<NumberDisplay
number={modifier}
type="signed"
isModified={hasModifiedValue}
/>
/>
</DigitalDiceWrapper>
)}
</div>
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import {
@@ -13,7 +12,9 @@ import {
Skill,
ValueLookup,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import a11yStyles from "~/styles/accessibility.module.css";
import { StyleSizeTypeEnum } from "../../../Shared/reducers/appEnv";
import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
@@ -30,7 +31,7 @@ interface Props {
theme: CharacterTheme;
diceEnabled: boolean;
ruleData: RuleData;
rollContext: IRollContext;
rollContext: RollContext;
}
export default class SkillsBox extends React.PureComponent<Props> {
static defaultProps = {
@@ -63,7 +64,7 @@ export default class SkillsBox extends React.PureComponent<Props> {
return (
<section className="ct-skills-box">
<BoxBackground StyleComponent={BoxBackgroundComponent} theme={theme} />
<h2 style={visuallyHidden}>Skills</h2>
<h2 className={a11yStyles.screenreaderOnly}>Skills</h2>
<Skills
skills={skills}
customSkills={customSkills}
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import {
@@ -14,6 +13,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { NumberDisplay } from "~/components/NumberDisplay";
import a11yStyles from "~/styles/accessibility.module.css";
interface Props {
speeds: SpeedInfo;
@@ -47,7 +47,7 @@ export default class SpeedBox extends React.PureComponent<Props> {
return (
<section className="ct-speed-box" onClick={this.handleSpeedsClick}>
<h2 style={visuallyHidden}>Speed</h2>
<h2 className={a11yStyles.screenreaderOnly}>Speed</h2>
<BoxBackground StyleComponent={BeveledBoxSvg94x89} theme={theme} />
<div
className={`ct-speed-box__heading ${
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class SpellsFilterAdvancedFilter extends React.PureComponent {
export default class SpellsFilterAdvancedFilter extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-spells-filter__adv-filter">{this.props.children}</div>
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class SpellsFilterAdvancedFilterLabel extends React.PureComponent {
export default class SpellsFilterAdvancedFilterLabel extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-spells-filter__adv-filter-label">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class SpellsFilterAdvancedFilterOption extends React.PureComponent {
export default class SpellsFilterAdvancedFilterOption extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-spells-filter__adv-filter-option">
@@ -1,6 +1,6 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
export default class SpellsFilterAdvancedFilterOptions extends React.PureComponent {
export default class SpellsFilterAdvancedFilterOptions extends PureComponent<PropsWithChildren> {
render() {
return (
<div className="ct-spells-filter__adv-filter-options">
@@ -1,12 +1,12 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
interface Props {
interface Props extends PropsWithChildren {
name?: string;
className: string;
}
export default class SubsectionMobile extends React.PureComponent<Props> {
export default class SubsectionMobile extends PureComponent<Props> {
static defaultProps = {
className: "",
};
@@ -1,12 +1,12 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
import { FormatUtils } from "@dndbeyond/character-rules-engine/es";
interface Props {
interface Props extends PropsWithChildren {
name?: string;
className: string;
}
export default class SubsectionTablet extends React.PureComponent<Props> {
export default class SubsectionTablet extends PureComponent<Props> {
static defaultProps = {
className: "",
};
@@ -1,9 +1,9 @@
import React from "react";
import { PureComponent, PropsWithChildren } from "react";
interface Props {
interface Props extends PropsWithChildren {
className: string;
}
export default class Subsections extends React.PureComponent<Props> {
export default class Subsections extends PureComponent<Props> {
static defaultProps = {
className: "",
};
@@ -1,11 +1,11 @@
import React from "react";
import React, { PropsWithChildren } from "react";
import { ManageIcon } from "@dndbeyond/character-components/es";
import { CharacterTheme } from "@dndbeyond/character-rules-engine/es";
import MobileDivider from "../MobileDivider";
interface Props {
interface Props extends PropsWithChildren {
header: React.ReactNode;
onHeaderClick?: () => void;
className: string;
@@ -10,7 +10,7 @@ interface Props {
traitKey: string;
label?: string;
fallback: string;
onClick?: (traitKey: string) => void;
onClick?: (traitKey: Constants.TraitTypeEnum) => void;
}
export default class TraitContent extends React.PureComponent<Props> {
static defaultProps = {
@@ -23,7 +23,7 @@ export default class TraitContent extends React.PureComponent<Props> {
if (onClick) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
onClick(traitKey);
onClick(traitKey as Constants.TraitTypeEnum);
}
};
@@ -1,202 +0,0 @@
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import CloseIcon from "@mui/icons-material/Close";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import Grid from "@mui/material/Grid";
import IconButton from "@mui/material/IconButton";
import Link from "@mui/material/Link";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { rulesEngineSelectors } from "@dndbeyond/character-rules-engine/es";
import characterGalleryImg from "~/images/character-gallery.png";
import characterSheetImg from "~/images/character-sheet.png";
import tadaEmoji from "~/images/tada-emoji.svg?url";
type Props = {
characterId: number;
campaignId?: number | null;
};
export const ClaimConfirmationDialog: React.FC<Props> = ({
characterId,
campaignId,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(true);
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
const { isDarkMode } = theme;
const close = () => {
window.location.href = `/characters/${characterId}`;
};
useEffect(() => {
characterId && setIsOpen(true);
}, [characterId]);
return (
<Dialog
onClose={close}
open={isOpen}
fullWidth={true}
sx={{
"& .MuiPaper-root": {
maxWidth: "644px",
backgroundColor: isDarkMode ? "#374045" : "white",
},
}}
>
<DialogTitle
sx={{
m: 0,
p: "24px 32px 16px",
fontSize: "18px",
fontWeight: 700,
color: isDarkMode ? "white" : "#12181C",
}}
>
<Box component="img" src={tadaEmoji} alt="Ta-da emoji" />{" "}
Congratulations!
<IconButton
aria-label="close"
onClick={close}
sx={{
position: "absolute",
right: 8,
top: 8,
color: isDarkMode
? "rgba(255, 255, 255, 0.54)"
: "rgba(0, 0, 0, 0.54)",
"&:hover": {
backgroundColor: isDarkMode
? "rgba(220, 223, 225, 0.08)"
: "#ECEDEE",
},
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ m: 0, p: "0 24px 24px" }}>
<Grid container justifyContent="center">
<DialogContentText
textAlign={"center"}
sx={{ color: isDarkMode ? "white" : "#12181C" }}
>
Your character has been claimed and is now available:
</DialogContentText>
<Grid item container spacing={2}>
<Grid
item
container
xs={12}
sm={6}
direction="column"
alignItems="center"
>
<Box
component="img"
sx={{ width: "100%" }}
src={characterGalleryImg}
alt="Premade character gallery"
></Box>
<Button
variant="outlined"
href="/characters"
sx={{
height: "40px",
width: "230px",
marginTop: "-20px",
color: "#374045",
fontSize: "12px",
lineHeight: "15px",
backgroundColor: "#fff",
border: "2px solid #3298ED",
borderRadius: "8px",
boxShadow: "none",
"&:hover": {
color: "#fff",
backgroundColor: "#232B2F",
border: "2px solid #3298ED",
boxShadow: "none",
},
}}
>
Go to my characters{" "}
<ArrowForwardIosIcon
sx={{ position: "absolute", right: "8px", height: "14px" }}
/>
</Button>
</Grid>
<Grid
item
container
xs={12}
sm={6}
direction="column"
alignItems="center"
>
<Box
component="img"
sx={{
width: "100%",
}}
src={characterSheetImg}
alt="Sample character sheet"
></Box>
<Button
variant="contained"
href={`/characters/${characterId}`}
sx={{
height: "40px",
width: "230px",
marginTop: "-20px",
color: "#fff",
fontSize: "12px",
lineHeight: "15px",
backgroundColor: "#3298ED",
borderRadius: "8px",
boxShadow: "none",
"&:hover": {
color: "#fff",
backgroundColor: "#53ADF0",
boxShadow: "none",
},
}}
>
View your character{" "}
<ArrowForwardIosIcon
sx={{ position: "absolute", right: "8px", height: "14px" }}
/>
</Button>
</Grid>
</Grid>
{campaignId && (
<Link
href={`/campaigns/${campaignId}`}
variant="button"
underline="none"
sx={{ marginTop: "20px", color: "#3298ED" }}
>
or return to your campaign
</Link>
)}
</Grid>
</DialogContent>
</Dialog>
);
};
export default ClaimConfirmationDialog;
@@ -1,222 +0,0 @@
import CloseIcon from "@mui/icons-material/Close";
import VideocamIcon from "@mui/icons-material/Videocam";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import IconButton from "@mui/material/IconButton";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { rulesEngineSelectors } from "@dndbeyond/character-rules-engine/es";
import { MaxCharactersDialog } from "~/components/MaxCharactersDialog";
import { useAuth } from "~/contexts/Authentication";
import { useClaimCharacter } from "~/hooks/useClaimCharacter";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import ClaimConfirmationDialog from "./ClaimConfirmationDialog";
export const WatchTourDialog: React.FC = () => {
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
const { isDarkMode } = theme;
const params = new URLSearchParams(globalThis.location.search);
const campaignJoinCode = params.get("campaignJoinCode");
const isAssigned = params.get("isAssigned") === "false" ? false : true;
const [isOpen, setIsOpen] = useState<boolean>(false);
const { characterSlotLimit, activeCharacterCount } = useSelector(
appEnvSelectors.getCharacterSlots
);
// Character slot limit is null for admin accounts
const hasOpenSlot =
characterSlotLimit === null || activeCharacterCount < characterSlotLimit;
const [isMaxCharacterMessageOpen, setIsMaxCharacterMessageOpen] =
useState(false);
const user = useAuth();
const signedIn = !!user;
const signupLink = `/create-account?returnUrl=${window.location.pathname}`;
const [
claimCharacter,
isClaimingCharacter,
isFinishedClaimingCharacter,
newCharacterId,
campaignId,
] = useClaimCharacter({
campaignJoinCode,
isAssigned,
});
const open = () => {
setIsOpen(true);
};
const close = () => {
setIsOpen(false);
};
const handleClaim = () => {
if (hasOpenSlot) {
claimCharacter();
} else {
setIsMaxCharacterMessageOpen(true);
setIsOpen(false);
}
};
useEffect(() => {
if (isFinishedClaimingCharacter) {
setIsOpen(false);
}
}, [isFinishedClaimingCharacter]);
return (
<>
<Button
variant="contained"
onClick={open}
sx={{
color: "white",
fontSize: "12px",
lineHeight: "15px",
width: "fit-content",
padding: "12px 32px",
backgroundColor: "#C50009",
borderRadius: "8px",
boxShadow: "none",
"&:hover": { backgroundColor: "#A7080B", boxShadow: "none" },
}}
>
<VideocamIcon sx={{ m: "-5px 0" }} />
&nbsp;Watch Tour
</Button>
<Dialog
onClose={close}
open={isOpen}
fullWidth={true}
maxWidth={"sm"}
sx={{
" .MuiPaper-root": {
backgroundColor: isDarkMode ? "#374045" : "white",
},
}}
>
<DialogTitle
sx={{
m: 0,
p: "24px 32px 8px",
fontSize: "18px",
fontWeight: 700,
color: isDarkMode ? "white" : "#12181C",
}}
>
D&D Beyond Character Sheet Tour
<IconButton
aria-label="close"
onClick={close}
sx={{
position: "absolute",
right: 8,
top: 8,
color: isDarkMode
? "rbga(255, 255, 255, 0.54)"
: "rgba(0, 0, 0, 0.54)",
"&:hover": "#ECEDEE",
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ m: 0, p: "0 32px 24px" }}>
<Box
noValidate
component="form"
sx={{
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<div
style={{
position: "relative",
width: "100%",
height: 0,
paddingBottom: "56.25%",
}}
>
<iframe
src="https://www.youtube-nocookie.com/embed/ChYPIdCrBdE"
title="D&D Beyond Sheet Tour"
style={{
border: 0,
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "gray",
}}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
></iframe>
</div>
<DialogContentText
sx={{
color: isDarkMode ? "white" : "#12181C",
" a": {
color: "#3298ED",
textDecoration: "none",
},
" a:hover": { color: "#53ADF0" },
}}
>
For a complete guide to getting your game started, see{" "}
<a href="https://www.dndbeyond.com/posts/754-how-to-play-dungeons-dragons-using-d-d-beyond">
How to Play Dungeons & Dragons Using D&D Beyond
</a>
.
</DialogContentText>
<Button
variant="contained"
disabled={isClaimingCharacter}
{...(!signedIn && { href: signupLink })}
{...(signedIn && { onClick: handleClaim })}
sx={{
color: "white",
fontSize: "12px",
lineHeight: "15px",
width: "fit-content",
padding: "12px 32px",
m: "auto",
backgroundColor: "#3298ED",
borderRadius: "8px",
boxShadow: "none",
"&:hover": { backgroundColor: "#53ADF0", boxShadow: "none" },
}}
>
Claim Character
</Button>
</Box>
</DialogContent>
</Dialog>
<MaxCharactersDialog
open={isMaxCharacterMessageOpen}
onClose={() => setIsMaxCharacterMessageOpen(false)}
useMyCharactersLink
/>
{isFinishedClaimingCharacter && !!newCharacterId && (
<ClaimConfirmationDialog
characterId={newCharacterId}
campaignId={campaignId}
/>
)}
</>
);
};
export default WatchTourDialog;
@@ -1,4 +0,0 @@
import WatchTourDialog from "./WatchTourDialog";
export default WatchTourDialog;
export { WatchTourDialog };