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
@@ -1,6 +1,6 @@
import { visuallyHidden } from "@mui/utils";
import React, { useContext } from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
AbilityLookup,
@@ -38,12 +38,13 @@ import {
SpellUtils,
WeaponSpellDamageGroup,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import { Link } from "~/components/Link";
import { TabFilter } from "~/components/TabFilter";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import {
PaneComponentEnum,
PaneIdentifiers,
@@ -79,10 +80,12 @@ interface ActionListConfig {
theme: CharacterTheme;
dataOriginRefData: DataOriginRefData;
proficiencyBonus: number;
rollContext: IRollContext;
rollContext: RollContext;
}
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
showNotes: boolean;
activatables: Array<Activatable>;
@@ -100,7 +103,7 @@ interface Props extends DispatchProp {
theme: CharacterTheme;
dataOriginRefData: DataOriginRefData;
proficiencyBonus: number;
characterRollContext: IRollContext;
characterRollContext: RollContext;
inventoryManager: InventoryManager;
paneHistoryStart: PaneInfo["paneHistoryStart"];
}
@@ -429,7 +432,7 @@ class Actions extends React.PureComponent<Props, State> {
const { attacksPerActionInfo, theme } = this.props;
return (
<React.Fragment>
<>
<div className="ct-actions__attacks-heading">
Actions &bull;{" "}
<span
@@ -447,7 +450,7 @@ class Actions extends React.PureComponent<Props, State> {
{attacksPerActionInfo.restriction}
</div>
)}
</React.Fragment>
</>
);
};
@@ -546,7 +549,7 @@ class Actions extends React.PureComponent<Props, State> {
return (
<section className="ct-actions">
<h2 style={visuallyHidden}>Actions</h2>
<h2 className={a11yStyles.screenreaderOnly}>Actions</h2>
<TabFilter
filters={[
...(!hasAttackGroups
@@ -7,7 +7,6 @@ import {
rulesEngineSelectors,
Item,
ItemUtils,
CampaignUtils,
CharacterTheme,
InventoryManager,
serviceDataSelectors,
@@ -93,33 +92,30 @@ class Attunement extends React.PureComponent<Props> {
</div>
)}
</div>
{partyInfo &&
CampaignUtils.isSharingStateActive(
CampaignUtils.getSharingState(partyInfo)
) && (
<>
<div className="ct-attunement__group-header">
Party Items Requiring Attunement
</div>
<div className="ct-attunement__group-items">
{sortedItems.party?.length > 0 ? (
sortedItems.party?.map((item) => (
<AttunementItem
key={ItemUtils.getUniqueKey(item)}
item={item}
onItemShow={this.handleItemShow}
theme={theme}
/>
))
) : (
<div className="ct-attunement__group-empty">
Party items that you can attune to will display here as you
make them active.
</div>
)}
</div>
</>
)}
{partyInfo && (
<>
<div className="ct-attunement__group-header">
Party Items Requiring Attunement
</div>
<div className="ct-attunement__group-items">
{sortedItems.party?.length > 0 ? (
sortedItems.party?.map((item) => (
<AttunementItem
key={ItemUtils.getUniqueKey(item)}
item={item}
onItemShow={this.handleItemShow}
theme={theme}
/>
))
) : (
<div className="ct-attunement__group-empty">
Party items that you can attune to will display here as you
make them active.
</div>
)}
</div>
</>
)}
</div>
);
};
@@ -1,49 +1,37 @@
import jss, { StyleSheet } from "jss";
import preset from "jss-preset-default";
import React from "react";
import { FC, useEffect, useState } from "react";
import { BackdropInfo } from "@dndbeyond/character-rules-engine/es";
import { DDB_MEDIA_URL } from "../../../../../constants";
interface Props {
interface BackdropStylesProps {
backdrop: BackdropInfo;
}
export default class BackdropStyles extends React.PureComponent<Props> {
sheet: StyleSheet | null = null;
componentDidMount() {
jss.setup(preset());
this.renderStyleSheet(this.props);
}
componentWillUnmount() {
this.removeStyleSheet();
}
componentDidUpdate(
prevProps: Readonly<Props>,
prevState: Readonly<{}>,
snapshot?: any
): void {
this.removeStyleSheet();
this.renderStyleSheet(this.props);
}
renderStyleSheet = (props: Props): void => {
const { backdrop } = props;
export const BackdropStyles: FC<BackdropStylesProps> = ({ backdrop }) => {
const [sheet, setSheet] = useState<StyleSheet | null>(null);
const renderStyleSheet = (): void => {
if (backdrop.backdropAvatarUrl !== null) {
this.sheet = jss.createStyleSheet({});
let tempSheet = jss.createStyleSheet({});
if (this.sheet !== null) {
const bodyStyles = window.getComputedStyle(document.body);
const newNavHeight = bodyStyles.getPropertyValue(
"--top-navigation-height"
);
const navHeightVar = newNavHeight
? "--top-navigation-height"
: "--ttui-site-nav-height";
if (tempSheet !== null) {
let breakpointRules: Record<string, string> = [
{ width: 768, height: 152, image: backdrop.backdropAvatarUrl },
{ width: 1024, height: 218, image: backdrop.backdropAvatarUrl },
{ width: 1200, height: 230, image: backdrop.backdropAvatarUrl },
{ width: 768, image: backdrop.backdropAvatarUrl },
{ width: 1024, image: backdrop.backdropAvatarUrl },
{ width: 1200, image: backdrop.backdropAvatarUrl },
{
width: 1921,
height: 230,
image:
backdrop.smallBackdropAvatarUrl === null
? ""
@@ -51,7 +39,6 @@ export default class BackdropStyles extends React.PureComponent<Props> {
},
{
width: 2561,
height: 230,
image:
backdrop.largeBackdropAvatarUrl === null
? ""
@@ -59,30 +46,42 @@ export default class BackdropStyles extends React.PureComponent<Props> {
},
].reduce((acc, breakpoint) => {
acc[`@media (min-width: ${breakpoint.width}px)`] = {
background: `url(${breakpoint.image}) no-repeat center ${breakpoint.height}px, url(${DDB_MEDIA_URL}/attachments/0/84/background_texture.png) #f9f9f9 !important`,
background: `url(${breakpoint.image}) no-repeat center calc(var(${navHeightVar}) + var(--sheet-header-height)), url(${DDB_MEDIA_URL}/attachments/0/84/background_texture.png) #f9f9f9 !important`,
};
return acc;
}, {});
this.sheet.addRules({
tempSheet.addRules({
"@global": {
"html body.body-rpgcharacter-sheet": breakpointRules,
},
});
this.sheet.attach();
tempSheet.attach();
setSheet(tempSheet);
}
}
};
removeStyleSheet = (): void => {
if (this.sheet) {
jss.removeStyleSheet(this.sheet);
this.sheet = null;
const removeStyleSheet = (): void => {
if (sheet) {
jss.removeStyleSheet(sheet);
setSheet(null);
}
};
render() {
return null;
}
}
useEffect(() => {
jss.setup(preset());
renderStyleSheet();
return () => {
removeStyleSheet();
};
}, []);
useEffect(() => {
removeStyleSheet();
renderStyleSheet();
}, [backdrop]);
return null;
};
@@ -1,4 +0,0 @@
import BackdropStyles from "./BackdropStyles";
export default BackdropStyles;
export { BackdropStyles };
@@ -1,6 +1,8 @@
import React from "react";
import clsx from "clsx";
import React, { FC } from "react";
import { connect, DispatchProp } from "react-redux";
import { Link as RouterLink } from "react-router-dom";
import { Link as RouterLink, useSearchParams } from "react-router-dom";
import { AnyAction } from "redux";
import { Tooltip } from "@dndbeyond/character-common-components/es";
import {
@@ -25,15 +27,17 @@ import {
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { WatchTourDialog } from "~/subApps/sheet/components/WatchTourDialog/WatchTourDialog";
import { appEnvSelectors } from "../../../Shared/selectors";
import { GameLogState } from "../../../Shared/stores/typings";
import WatchTourDialog from "../../components/WatchTourDialog";
import { sheetAppSelectors } from "../../selectors";
import { SheetAppState } from "../../typings";
import CharacterHeaderInfo from "../CharacterHeaderInfo";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
campaign: CampaignDataContract | null;
builderUrl: string;
items: Array<Item>;
@@ -45,88 +49,69 @@ interface Props extends DispatchProp {
paneHistoryStart: PaneInfo["paneHistoryStart"];
}
class CharacterHeaderDesktop extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
}
handleCampaignShow = (): void => {
const { paneHistoryStart } = this.props;
const CharacterHeaderDesktop: FC<Props> = ({
paneHistoryStart,
items,
builderUrl,
campaign,
preferences,
isReadonly,
theme,
gameLog,
status,
}) => {
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
const handleCampaignShow = (): void => {
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
};
handleShortResetClick = (evt: React.MouseEvent): void => {
const { paneHistoryStart } = this.props;
const handleShortResetClick = (evt: React.MouseEvent): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(PaneComponentEnum.SHORT_REST);
};
handleLongResetClick = (evt: React.MouseEvent): void => {
const { paneHistoryStart } = this.props;
const handleLongResetClick = (evt: React.MouseEvent): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(PaneComponentEnum.LONG_REST);
};
handleShareClick = (evt: React.MouseEvent): void => {
const { paneHistoryStart } = this.props;
const handleShareClick = (evt: React.MouseEvent): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(PaneComponentEnum.SHARE_URL);
};
hasMagicItem = (): boolean => {
const { items } = this.props;
const hasMagicItem = (): boolean => {
return !!items.find((item) => ItemUtils.isMagic(item));
};
renderSideContent = (): React.ReactNode => {
const {
builderUrl,
campaign,
preferences,
isReadonly,
theme,
gameLog,
status,
} = this.props;
const renderSideContent = (): React.ReactNode => {
if (isReadonly) {
if (status === CharacterStatusSlug.PREMADE) {
return <WatchTourDialog />;
}
if (!campaign) {
return null;
}
return (
<CampaignSummary
campaign={campaign}
onCampaignShow={this.handleCampaignShow}
onCampaignShow={handleCampaignShow}
gameLog={gameLog}
theme={theme}
isDesktop
/>
);
}
return (
<React.Fragment>
<>
{preferences !== null &&
preferences.privacyType ===
Constants.PreferencePrivacyTypeEnum.PUBLIC && (
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--share">
<div
className="ct-character-header-desktop__button"
onClick={this.handleShareClick}
onClick={handleShareClick}
role="button"
tabIndex={0}
>
@@ -142,7 +127,7 @@ class CharacterHeaderDesktop extends React.PureComponent<Props> {
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--short-rest">
<div
className="ct-character-header-desktop__button"
onClick={this.handleShortResetClick}
onClick={handleShortResetClick}
role="button"
tabIndex={0}
>
@@ -156,8 +141,11 @@ class CharacterHeaderDesktop extends React.PureComponent<Props> {
</div>
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--long-rest">
<div
className="ct-character-header-desktop__button"
onClick={this.handleLongResetClick}
className={clsx([
"ct-character-header-desktop__button",
isVttView && "ct-character-header-desktop__button--vttView",
])}
onClick={handleLongResetClick}
role="button"
tabIndex={0}
>
@@ -177,45 +165,46 @@ class CharacterHeaderDesktop extends React.PureComponent<Props> {
{/*</div>*/}
{/*</div>*/}
{/*}*/}
{campaign !== null && (
{!isVttView && (
<CampaignSummary
campaign={campaign}
onCampaignShow={this.handleCampaignShow}
onCampaignShow={handleCampaignShow}
gameLog={gameLog}
theme={theme}
isDesktop
/>
)}
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--builder">
<Tooltip
isDarkMode={theme.isDarkMode}
title="Go to builder"
className="ct-character-header-desktop__builder"
>
<RouterLink
to={builderUrl}
className="ct-character-header-desktop__builder-link"
aria-label="Go to builder"
{!isVttView && (
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--builder">
<Tooltip
isDarkMode={theme.isDarkMode}
title="Go to builder"
className="ct-character-header-desktop__builder"
>
<LightBuilderSvg />
</RouterLink>
</Tooltip>
</div>
</React.Fragment>
<RouterLink
to={builderUrl}
className="ct-character-header-desktop__builder-link"
aria-label="Go to builder"
>
<LightBuilderSvg />
</RouterLink>
</Tooltip>
</div>
)}
</>
);
};
render() {
return (
<div className="ct-character-header-desktop">
<div className="ct-character-header-desktop__group ct-character-header-desktop__group-tidbits">
<CharacterHeaderInfo />
</div>
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--gap" />
{this.renderSideContent()}
return (
<div className="ct-character-header-desktop">
<div className="ct-character-header-desktop__group ct-character-header-desktop__group-tidbits">
<CharacterHeaderInfo />
</div>
);
}
}
<div className="ct-character-header-desktop__group ct-character-header-desktop__group--gap" />
{renderSideContent()}
</div>
);
};
function mapStateToProps(state: SheetAppState) {
return {
@@ -0,0 +1,4 @@
import CharacterHeaderDesktop from "./CharacterHeaderDesktop";
export default CharacterHeaderDesktop;
export { CharacterHeaderDesktop };
@@ -31,6 +31,7 @@ interface Props {
preferences: CharacterPreferences;
decorationInfo: DecorationInfo;
isReadonly: boolean;
isVttView?: boolean;
paneHistoryStart: PaneInfo["paneHistoryStart"];
}
class CharacterHeaderInfo extends React.PureComponent<Props, {}> {
@@ -54,6 +55,7 @@ class CharacterHeaderInfo extends React.PureComponent<Props, {}> {
ruleData,
decorationInfo,
isReadonly,
isVttView,
} = this.props;
return (
@@ -73,10 +75,13 @@ class CharacterHeaderInfo extends React.PureComponent<Props, {}> {
ruleData={ruleData}
xpInfo={xpInfo}
isInteractive={!isReadonly}
isVttView={isVttView}
calloutNode={
<ThemeButton size="small" style="outline">
Manage
</ThemeButton>
!isVttView && (
<ThemeButton size="small" style="outline">
Manage
</ThemeButton>
)
}
/>
</div>
@@ -1,5 +1,8 @@
import React from "react";
import clsx from "clsx";
import React, { FC } from "react";
import { connect, DispatchProp } from "react-redux";
import { useSearchParams } from "react-router-dom";
import { AnyAction } from "redux";
import {
rulesEngineSelectors,
@@ -9,19 +12,27 @@ import {
Constants,
HitPointInfo,
RuleData,
CharacterTheme,
CampaignDataContract,
} from "@dndbeyond/character-rules-engine/es";
import ListTimeline from "@dndbeyond/fontawesome-cache/svgs/light/list-timeline.svg";
import { GameLogNotificationWrapper } from "@dndbeyond/game-log-components";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { WatchTourDialog } from "~/subApps/sheet/components/WatchTourDialog";
import { GameLogState } from "~/tools/js/Shared/stores/typings";
import { appEnvSelectors } from "../../../Shared/selectors";
import StatusSummaryMobile from "../../components/StatusSummaryMobile";
import WatchTourDialog from "../../components/WatchTourDialog";
import { SheetAppState } from "../../typings";
import CharacterHeaderInfo from "../CharacterHeaderInfo";
import styles from "./styles.module.css";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
hitPointInfo: HitPointInfo;
fails: number;
successes: number;
@@ -31,63 +42,100 @@ interface Props extends DispatchProp {
isReadonly: boolean;
status: CharacterStatusSlug | null;
paneHistoryStart: PaneInfo["paneHistoryStart"];
gameLog?: GameLogState;
theme: CharacterTheme;
campaign: CampaignDataContract | null;
}
class CharacterHeaderMobile extends React.PureComponent<Props> {
handleHealthSummaryClick = (): void => {
const { paneHistoryStart, isReadonly } = this.props;
const CharacterHeaderMobile: FC<Props> = ({
paneHistoryStart,
dispatch,
hitPointInfo,
fails,
successes,
deathCause,
inspiration,
ruleData,
isReadonly,
status,
gameLog,
theme,
campaign,
}) => {
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
const handleHealthSummaryClick = (): void => {
if (!isReadonly) {
paneHistoryStart(PaneComponentEnum.HEALTH_MANAGE);
}
};
handleInspirationClick = (): void => {
const { inspiration, dispatch } = this.props;
const handleInspirationClick = (): void => {
dispatch(characterActions.inspirationSet(!inspiration));
};
const handleGameLogClick = (evt: React.MouseEvent): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
// Assuming dispatch is available in props or context
paneHistoryStart(PaneComponentEnum.GAME_LOG);
};
render() {
const {
hitPointInfo,
fails,
successes,
deathCause,
inspiration,
ruleData,
isReadonly,
status,
} = this.props;
return (
<div className="ct-character-header-mobile">
<div className="ct-character-header-mobile__group ct-character-header-mobile__group-tidbits">
<CharacterHeaderInfo />
</div>
<div className="ct-character-header-mobile__group ct-character-header-mobile__group--gap" />
{isReadonly && status === CharacterStatusSlug.PREMADE ? (
<div>
<WatchTourDialog />
</div>
) : (
<div className="ct-character-header-mobile__group ct-character-header-mobile__group--summary">
<StatusSummaryMobile
hitPointInfo={hitPointInfo}
fails={fails}
successes={successes}
deathCause={deathCause}
inspiration={inspiration}
ruleData={ruleData}
onHealthClick={this.handleHealthSummaryClick}
onInspirationClick={this.handleInspirationClick}
isInteractive={!isReadonly}
/>
</div>
)}
return (
<div
className={clsx([
"ct-character-header-mobile",
isVttView && "ct-character-header-mobile--vttView",
])}
>
<div
className={clsx([
"ct-character-header-mobile__group",
"ct-character-header-mobile__group-tidbits",
isVttView && "ct-character-header-mobile__group-tidbits--vttView",
])}
>
<CharacterHeaderInfo isVttView={isVttView} />
</div>
);
}
}
{!isVttView && !isReadonly && (
<div className={styles.campaignButtonContainer}>
<GameLogNotificationWrapper
themeColor={theme.themeColor}
gameLogIsOpen={gameLog?.isOpen ?? false}
notificationOnClick={handleGameLogClick}
>
<div
role="button"
aria-roledescription="Game Log"
className={clsx(styles.campaignButtonGroup)}
onClick={handleGameLogClick}
>
<div className={clsx(styles.campaignButton)}>
<ListTimeline className={clsx(styles.campaignButtonIcon)} />
</div>
</div>
</GameLogNotificationWrapper>
</div>
)}
{isReadonly && status === CharacterStatusSlug.PREMADE ? (
<div>
<WatchTourDialog />
</div>
) : (
<div className="ct-character-header-mobile__group ct-character-header-mobile__group--summary">
<StatusSummaryMobile
hitPointInfo={hitPointInfo}
fails={fails}
successes={successes}
deathCause={deathCause}
inspiration={inspiration}
ruleData={ruleData}
onHealthClick={handleHealthSummaryClick}
onInspirationClick={handleInspirationClick}
isInteractive={!isReadonly}
/>
</div>
)}
</div>
);
};
function mapStateToProps(state: SheetAppState) {
return {
@@ -99,6 +147,9 @@ function mapStateToProps(state: SheetAppState) {
deathCause: rulesEngineSelectors.getDeathCause(state),
isReadonly: appEnvSelectors.getIsReadonly(state),
status: characterSelectors.getStatusSlug(state),
theme: rulesEngineSelectors.getCharacterTheme(state),
gameLog: appEnvSelectors.getGameLog(state),
campaign: rulesEngineSelectors.getCampaign(state),
};
}
@@ -1,5 +1,8 @@
import React from "react";
import clsx from "clsx";
import React, { FC } from "react";
import { connect, DispatchProp } from "react-redux";
import { useSearchParams } from "react-router-dom";
import { AnyAction } from "redux";
import { Tooltip } from "@dndbeyond/character-common-components/es";
import {
@@ -22,20 +25,26 @@ import {
RuleData,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import ListTimeline from "@dndbeyond/fontawesome-cache/svgs/light/list-timeline.svg";
import { GameLogNotificationWrapper } from "@dndbeyond/game-log-components";
import { Link } from "~/components/Link";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { WatchTourDialog } from "~/subApps/sheet/components/WatchTourDialog";
import { GameLogState } from "~/tools/js/Shared/stores/typings";
import { appEnvSelectors } from "../../../Shared/selectors";
import StatusSummaryMobile from "../../components/StatusSummaryMobile";
import WatchTourDialog from "../../components/WatchTourDialog";
import { sheetAppSelectors } from "../../selectors";
import { SheetAppState } from "../../typings";
import { CharacterHeaderInfo } from "../CharacterHeaderInfo";
import styles from "./styles.module.css";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
hitPointInfo: HitPointInfo;
fails: number;
successes: number;
@@ -50,84 +59,67 @@ interface Props extends DispatchProp {
theme: CharacterTheme;
status: CharacterStatusSlug | null;
paneHistoryStart: PaneInfo["paneHistoryStart"];
gameLog?: GameLogState;
}
class CharacterHeaderTablet extends React.PureComponent<Props> {
handleCampaignShow = (): void => {
const { paneHistoryStart } = this.props;
const CharacterHeaderTablet: FC<Props> = ({
paneHistoryStart,
isReadonly,
dispatch,
inspiration,
items,
hitPointInfo,
fails,
successes,
deathCause,
builderUrl,
preferences,
ruleData,
theme,
status,
gameLog,
campaign,
}) => {
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
const handleCampaignShow = (): void => {
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
};
handleShortResetClick = (evt: React.MouseEvent): void => {
const { paneHistoryStart } = this.props;
const handleShortResetClick = (evt: React.MouseEvent): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(PaneComponentEnum.SHORT_REST);
};
handleGameLogClick = (evt: React.MouseEvent): void => {
const { paneHistoryStart } = this.props;
const handleGameLogClick = (evt: React.MouseEvent): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(PaneComponentEnum.GAME_LOG);
};
handleLongResetClick = (evt: React.MouseEvent): void => {
const { paneHistoryStart } = this.props;
const handleLongResetClick = (evt: React.MouseEvent): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(PaneComponentEnum.LONG_REST);
};
handleShareClick = (evt: React.MouseEvent): void => {
const { paneHistoryStart } = this.props;
const handleShareClick = (evt: React.MouseEvent): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(PaneComponentEnum.SHARE_URL);
};
handleHealthSummaryClick = (): void => {
const { paneHistoryStart, isReadonly } = this.props;
const handleHealthSummaryClick = (): void => {
if (!isReadonly) {
paneHistoryStart(PaneComponentEnum.HEALTH_MANAGE);
}
};
handleInspirationClick = (): void => {
const { inspiration, dispatch } = this.props;
const handleInspirationClick = (): void => {
dispatch(characterActions.inspirationSet(!inspiration));
};
hasMagicItem = (): boolean => {
const { items } = this.props;
const hasMagicItem = (): boolean => {
return !!items.find((item) => ItemUtils.isMagic(item));
};
renderSideContent = (): React.ReactNode => {
const {
hitPointInfo,
fails,
successes,
deathCause,
inspiration,
builderUrl,
preferences,
isReadonly,
ruleData,
theme,
status,
} = this.props;
const renderSideContent = (): React.ReactNode => {
if (isReadonly) {
if (status === CharacterStatusSlug.PREMADE) {
return (
@@ -137,7 +129,7 @@ class CharacterHeaderTablet extends React.PureComponent<Props> {
);
}
return (
<React.Fragment>
<>
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--summary">
<StatusSummaryMobile
hitPointInfo={hitPointInfo}
@@ -146,24 +138,24 @@ class CharacterHeaderTablet extends React.PureComponent<Props> {
inspiration={inspiration}
ruleData={ruleData}
deathCause={deathCause}
onHealthClick={this.handleHealthSummaryClick}
onInspirationClick={this.handleInspirationClick}
onHealthClick={handleHealthSummaryClick}
onInspirationClick={handleInspirationClick}
isInteractive={!isReadonly}
/>
</div>
</React.Fragment>
</>
);
}
return (
<React.Fragment>
<>
{preferences !== null &&
preferences.privacyType ===
Constants.PreferencePrivacyTypeEnum.PUBLIC && (
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--share">
<div
className="ct-character-header-tablet__button"
onClick={this.handleShareClick}
onClick={handleShareClick}
role="button"
tabIndex={0}
>
@@ -176,10 +168,16 @@ class CharacterHeaderTablet extends React.PureComponent<Props> {
</div>
</div>
)}
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--short-rest">
<div
className={clsx([
"ct-character-header-tablet__group",
"ct-character-header-tablet__group--short-rest",
styles.shortRest,
])}
>
<div
className="ct-character-header-tablet__button"
onClick={this.handleShortResetClick}
onClick={handleShortResetClick}
role="button"
tabIndex={0}
>
@@ -194,7 +192,7 @@ class CharacterHeaderTablet extends React.PureComponent<Props> {
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--long-rest">
<div
className="ct-character-header-tablet__button"
onClick={this.handleLongResetClick}
onClick={handleLongResetClick}
role="button"
tabIndex={0}
>
@@ -206,6 +204,26 @@ class CharacterHeaderTablet extends React.PureComponent<Props> {
</span>
</div>
</div>
{!isVttView && (
<div className={styles.campaignButtonContainer}>
<GameLogNotificationWrapper
themeColor={theme.themeColor}
gameLogIsOpen={gameLog?.isOpen ?? false}
notificationOnClick={handleGameLogClick}
>
<div
role="button"
aria-roledescription="Game Log"
className={clsx(styles.campaignButtonGroup)}
onClick={handleGameLogClick}
>
<div className={clsx(styles.campaignButton)}>
<ListTimeline className={clsx(styles.campaignButtonIcon)} />
</div>
</div>
</GameLogNotificationWrapper>
</div>
)}
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--builder">
<Tooltip
isDarkMode={theme.isDarkMode}
@@ -215,7 +233,7 @@ class CharacterHeaderTablet extends React.PureComponent<Props> {
<Link
href={builderUrl}
className="ct-character-header-tablet__builder-link"
useRouter
userouter
>
<LightBuilderSvg />
</Link>
@@ -229,27 +247,24 @@ class CharacterHeaderTablet extends React.PureComponent<Props> {
deathCause={deathCause}
inspiration={inspiration}
ruleData={ruleData}
onHealthClick={this.handleHealthSummaryClick}
onInspirationClick={this.handleInspirationClick}
onHealthClick={handleHealthSummaryClick}
onInspirationClick={handleInspirationClick}
isInteractive={!isReadonly}
/>
</div>
</React.Fragment>
</>
);
};
render() {
return (
<div className="ct-character-header-tablet">
<div className="ct-character-header-tablet__group ct-character-header-tablet__group-tidbits">
<CharacterHeaderInfo />
</div>
<div className="ct-character-header-tablet__group ct-character-header-tablet__group--gap" />
{this.renderSideContent()}
return (
<div className="ct-character-header-tablet">
<div className="ct-character-header-tablet__group ct-character-header-tablet__group-tidbits">
<CharacterHeaderInfo />
</div>
);
}
}
{renderSideContent()}
</div>
);
};
function mapStateToProps(state: SheetAppState) {
return {
@@ -266,6 +281,7 @@ function mapStateToProps(state: SheetAppState) {
deathCause: rulesEngineSelectors.getDeathCause(state),
theme: rulesEngineSelectors.getCharacterTheme(state),
status: characterSelectors.getStatusSlug(state),
gameLog: appEnvSelectors.getGameLog(state),
};
}
@@ -1,6 +1,9 @@
import clsx from "clsx";
import { throttle } from "lodash";
import { createRef, PureComponent } from "react";
import { connect, DispatchProp } from "react-redux";
import { useSearchParams } from "react-router-dom";
import { AnyAction } from "redux";
import {
characterEnvSelectors,
@@ -11,13 +14,15 @@ import {
CharacterPreferences,
characterSelectors,
CharacterStatusSlug,
CampaignDataContract,
} from "@dndbeyond/character-rules-engine/es";
import { GameLogContextProvider } from "@dndbeyond/game-log-components";
import { Link } from "~/components/Link";
import { NotificationSystem } from "~/components/NotificationSystem";
import { PremadeCharacterEditStatus } from "~/components/PremadeCharacterEditStatus";
import { usePositioning } from "~/hooks/usePositioning";
import { CharacterSheetGuidedTour } from "~/subApps/sheet/components/CharacterSheetGuidedTour";
import { ClaimPremadeButton } from "~/subApps/sheet/components/ClaimPremadeButton";
import { MobileNav } from "~/subApps/sheet/components/MobileNav";
import { useHeadContext } from "../../../../../contexts/Head";
@@ -43,7 +48,6 @@ import {
} from "../../../Shared/stores/typings";
import config from "../../../config";
import { sheetActions } from "../../actions";
import ClaimPremadeButton from "../../components/ClaimPremadeButton";
import { InvalidCharacter } from "../../components/InvalidCharacter";
import {
DESKTOP_COMPONENT_START_WIDTH,
@@ -53,11 +57,13 @@ import {
import { sheetAppSelectors, sheetSelectors } from "../../selectors";
import { SheetAppState, SheetPositioningInfo } from "../../typings";
import CharacterSheetDesktop from "../CharacterSheetDesktop";
import CharacterSheetGuidedTour from "../CharacterSheetGuidedTour";
import CharacterSheetMobile from "../CharacterSheetMobile";
import CharacterSheetTablet from "../CharacterSheetTablet";
import styles from "./styles.module.css";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
sheetPosition: SheetPositioningInfo;
initFailed: boolean;
loadingStatus: Constants.CharacterLoadingStatusEnum;
@@ -81,8 +87,11 @@ interface Props extends DispatchProp {
preferences: CharacterPreferences;
setManager: (manager: any) => void;
setTitle: (title: string) => void;
isVttView: boolean;
characterStatus: CharacterStatusSlug | null;
userRoles: UserRoles;
userId: number;
campaign: CampaignDataContract | null;
}
interface State {
curseHeaderHeight: number;
@@ -146,6 +155,8 @@ class CharacterSheet extends PureComponent<Props, State> {
characterStatus,
username,
userRoles,
userId,
campaign,
} = this.props;
if (loadingStatus === Constants.CharacterLoadingStatusEnum.LOADED) {
@@ -163,6 +174,7 @@ class CharacterSheet extends PureComponent<Props, State> {
appEnvActions.dataSet({
isReadonly: !canEdit,
diceEnabled: canEdit ? diceEnabled : false,
isUserDM: Number(userId) === campaign?.dmUserId,
})
);
}
@@ -316,7 +328,7 @@ class CharacterSheet extends PureComponent<Props, State> {
let children: React.ReactNode;
if (Component) {
let childrenProps = componentProps ? componentProps : {};
let childrenProps = componentProps || {};
children = <Component {...childrenProps} />;
}
@@ -449,15 +461,12 @@ class CharacterSheet extends PureComponent<Props, State> {
case AppErrorTypeEnum.ACCESS_DENIED:
contentNode = (
<>
<p>You don't have access to this page.</p>
<p>
If you are trying to access a character sheet, make sure its
privacy setting is configured correctly for you to access it.
</p>
<p>
It is also possible that you are trying to enter the 403rd level
of the endless dungeon and the ancient dragon Rylzrayrth is rising
up to block your path...
<p style={{ margin: 0 }}>You reach for the sheet,</p>
<p style={{ margin: 0 }}>But the sigil reads "Private"</p>
<p style={{ margin: 0 }}>Permission denied.</p>
<p style={{ marginTop: 15 }}>
Ask the owner to set their Character Sheet to Campaign Only or
Public in their character's settings.
</p>
</>
);
@@ -560,6 +569,7 @@ class CharacterSheet extends PureComponent<Props, State> {
theme,
isReadonly,
characterStatus,
isVttView,
} = this.props;
let sheetStyles: React.CSSProperties = {};
@@ -591,46 +601,43 @@ class CharacterSheet extends PureComponent<Props, State> {
return (
<CharacterSheetGuidedTour>
<GameLogContextProvider
baseUrl={gameLog.apiEndpoint}
ddbApiUrl={gameLog.ddbApiEndpoint}
diceServiceUrl={diceFeatureConfiguration.apiEndpoint}
authUrl={authEndpoint}
diceThumbnailsUrl={`${diceFeatureConfiguration.assetBaseLocation}/images/thumbnails`}
entityId={characterId ? characterId.toString() : ""}
>
<div className={classNames.join(" ")}>
{this.renderSyncBlocker()}
<div
className="ct-character-sheet__inner"
ref={this.sheetRef}
style={sheetStyles}
<div className={classNames.join(" ")}>
{this.renderSyncBlocker()}
<div
className={clsx([
"ct-character-sheet__inner",
!everythingIsLoadedWithoutErrors && styles.sheetNotReady,
])}
ref={this.sheetRef}
style={sheetStyles}
>
<ThemeManagerProvider
manager={{
lightOrDark: theme.isDarkMode ? "dark" : "light",
primary: theme.themeColor,
}}
>
<ThemeManagerProvider
manager={{
lightOrDark: theme.isDarkMode ? "dark" : "light",
primary: theme.themeColor,
}}
>
{everythingIsLoadedWithoutErrors && (
<PremadeCharacterEditStatus
characterStatus={characterStatus}
isReadonly={isReadonly}
/>
)}
{this.renderContent()}
{everythingIsLoadedWithoutErrors && <MobileNav />}
</ThemeManagerProvider>
</div>
<NotificationSystem ref={this.notificationSystem} />
{diceEnabled && !isReadonly && (
<DiceContainer canShow={everythingIsLoadedWithoutErrors} />
)}
{everythingIsLoadedWithoutErrors &&
characterStatus === CharacterStatusSlug.PREMADE &&
isReadonly && <ClaimPremadeButton />}
{everythingIsLoadedWithoutErrors && (
<PremadeCharacterEditStatus
characterStatus={characterStatus}
isReadonly={isReadonly}
/>
)}
{this.renderContent()}
{everythingIsLoadedWithoutErrors && <MobileNav />}
</ThemeManagerProvider>
</div>
</GameLogContextProvider>
<NotificationSystem ref={this.notificationSystem} />
{diceEnabled && !isReadonly && (
<DiceContainer
canShow={everythingIsLoadedWithoutErrors}
isVttView={isVttView}
/>
)}
{everythingIsLoadedWithoutErrors &&
characterStatus === CharacterStatusSlug.PREMADE &&
isReadonly && <ClaimPremadeButton />}
</div>
</CharacterSheetGuidedTour>
);
}
@@ -639,11 +646,14 @@ class CharacterSheet extends PureComponent<Props, State> {
const CharacterSheetWrapper = (props) => {
const { setTitle } = useHeadContext();
const { getSheetPositioning } = usePositioning();
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
return (
<CharacterSheet
{...props}
setTitle={setTitle}
sheetPosition={getSheetPositioning()}
isVttView={isVttView}
/>
);
};
@@ -671,6 +681,8 @@ function mapStateToProps(state: SheetAppState) {
characterName: rulesEngineSelectors.getName(state),
preferences: rulesEngineSelectors.getPreferences(state),
characterStatus: characterSelectors.getStatusSlug(state),
userId: appEnvSelectors.getUserId(state),
campaign: characterSelectors.getCampaign(state),
};
}
export default connect(mapStateToProps)(CharacterSheetWrapper);
@@ -1,6 +1,7 @@
import { spring, TransitionMotion } from "@serprex/react-motion";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import { ThemeStyles } from "@dndbeyond/character-components/es";
import {
@@ -20,7 +21,7 @@ import { AppEnvDimensionsState } from "../../../Shared/stores/typings";
import Subsections from "../../components/Subsections";
import { DESKTOP_LARGE_COMPONENT_START_WIDTH } from "../../config";
import { SheetAppState } from "../../typings";
import BackdropStyles from "../BackdropStyles";
import { BackdropStyles } from "../BackdropStyles";
import { CharacterHeaderDesktop } from "../CharacterHeaderDesktop";
import Combat from "../Combat";
import PrimaryBox from "../PrimaryBox";
@@ -30,7 +31,9 @@ import SavingThrowsDesktop from "../SavingThrowsDesktop";
import SensesDesktop from "../SensesDesktop";
import SkillsDesktop from "../SkillsDesktop";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
sidebarInfo: SidebarInfo;
sidebarPosition: SidebarPositionInfo;
decorationInfo: DecorationInfo;
@@ -64,7 +67,7 @@ class CharacterSheetDesktop extends React.PureComponent<Props, State> {
renderContent = (): React.ReactNode => {
return (
<React.Fragment>
<>
<CharacterHeaderDesktop />
<QuickInfo />
<Subsections>
@@ -75,7 +78,7 @@ class CharacterSheetDesktop extends React.PureComponent<Props, State> {
<Combat />
<PrimaryBox />
</Subsections>
</React.Fragment>
</>
);
};
@@ -100,22 +103,20 @@ class CharacterSheetDesktop extends React.PureComponent<Props, State> {
]}
>
{(interpolatedStyles) => (
<React.Fragment>
<>
{interpolatedStyles.map((config) => (
<>
<Sidebar
key={config.key}
style={{
...sidebarPosition,
transform: `translateX(${config.style.transform}px)`,
}}
setSwipedAmount={(swipedAmount) =>
this.setState({ swipedAmount })
}
/>
</>
<Sidebar
key={config.key}
style={{
...sidebarPosition,
transform: `translateX(${config.style.transform}px)`,
}}
setSwipedAmount={(swipedAmount) =>
this.setState({ swipedAmount })
}
/>
))}
</React.Fragment>
</>
)}
</TransitionMotion>
</div>
@@ -147,13 +148,13 @@ class CharacterSheetDesktop extends React.PureComponent<Props, State> {
const { decorationInfo } = this.props;
return (
<React.Fragment>
<>
{this.renderSheet()}
<BackdropStyles
backdrop={DecorationUtils.getBackdropInfo(decorationInfo)}
/>
<ThemeStyles decorationInfo={decorationInfo} />
</React.Fragment>
</>
);
}
}
@@ -1,74 +0,0 @@
import { useMediaQuery } from "@mui/material";
import { useContext, useEffect, useState } from "react";
import { connect, useSelector } from "react-redux";
import {
Constants,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine";
import { useFeatureFlags } from "~/contexts/FeatureFlag";
import { GuidedTour } from "../../../Shared/containers/GuidedTour";
import { getCharacterSheetSteps } from "./getCharacterSheetSteps";
const CharacterSheetGuidedTour = ({ children }) => {
const [step, setStep] = useState(0);
const { characterSheetTourFlag } = useFeatureFlags();
const isTablet = useMediaQuery("(min-width: 768px)");
const isDesktop = useMediaQuery("(min-width: 1024px)");
const ready = useSelector(rulesEngineSelectors.isCharacterSheetReady);
// const hasSpells = useSelector(rulesEngineSelectors.hasSpells);
const [loading, setLoading] = useState(true);
const setCurrentStep = (currentStep) => {
switch (currentStep) {
case 11: {
if (!isTablet && !isDesktop) {
const toggle = document.querySelector(
"[class^='styles_navToggle']"
) as HTMLButtonElement;
toggle?.click();
}
break;
}
case 13: {
if (isTablet && !isDesktop) {
const toggle = document.querySelector(
"[class^='styles_navToggle']"
) as HTMLButtonElement;
toggle?.click();
}
break;
}
default:
break;
}
setStep(currentStep);
};
useEffect(() => {
// If character sheet has loaded, enable tour
if (ready) setLoading(false);
}, [ready]);
return (
<>
{characterSheetTourFlag && !loading ? (
<GuidedTour
steps={getCharacterSheetSteps(false, isTablet, isDesktop)}
step={step}
setStep={setCurrentStep}
showOnFirstLoad={true}
cookieName="characterSheetGuidedTour"
>
{children}
</GuidedTour>
) : (
<>{children}</>
)}
</>
);
};
export default CharacterSheetGuidedTour;
@@ -1,457 +0,0 @@
import { StepType } from "@reactour/tour";
import { GuidedTourStep } from "~/tools/js/Shared/containers/GuidedTour";
/**
* CHARACTER SHEET TOUR STEPS
**/
export const getCharacterSheetSteps = (
hasSpells: boolean,
isTablet: boolean,
isDesktop: boolean
): StepType[] => {
const getSelector = (
mobileSelector: string,
tabletSelector?: string,
desktopSelector?: string
) =>
isDesktop && desktopSelector
? desktopSelector
: isTablet && tabletSelector
? tabletSelector
: mobileSelector;
const stepList = [
{
selector: getSelector("html"),
content: (
<GuidedTourStep
title="Welcome!"
content={
<>
Welcome to your character sheet! Here, you can find information
about your character, roll dice, and find more information about
other characters in your game. If you ever have any questions
about rules or what something is for, you can click on almost
anything to learn more!
</>
}
showClose
/>
),
},
{
selector: getSelector(".dice-toolbar"),
content: (
<GuidedTourStep
title="Digital Dice"
content={
<>
Click here if you need to make a custom dice roll. Your dice
collection can be found{" "}
<a
href="https://www.dndbeyond.com/my-dice"
target="_blank"
rel="noopener"
>
here
</a>
.
</>
}
/>
),
},
{
selector: getSelector(
".ct-character-header-mobile__group-tidbits",
".ct-character-header-tablet__group-tidbits",
".ct-character-header-desktop__group-tidbits"
),
content: (
<GuidedTourStep
title="Character Overview"
content={
<>
Basic information about your character such as Name, Species,
Class, and Level can be found here. Click this area to change your
character's name, sheet styles, and modify other settings for your
character.
</>
}
/>
),
},
{
selector: ".ct-character-header-desktop__group--builder",
content: (
<GuidedTourStep
title="Character Builder"
content={
<>
Click here to visit the Character Builder. Each time you level up,
check here to see how to develop your character.
</>
}
/>
),
},
{
selector: getSelector(
".ct-main-mobile__abilities",
".ct-main-tablet__abilities",
".ct-quick-info__abilities"
),
content: (
<GuidedTourStep
title="Ability Scores"
content={
<>
Much of what your character does in the game depends on his or her
six abilities: Strength, Dexterity, Constitution, Intelligence,
Wisdom, and Charisma. Learn more about an ability by clicking on
it.
</>
}
/>
),
},
{
selector: getSelector(
".ct-combat-mobile__extra--proficiency",
".ct-combat-tablet__extra--proficiency",
".ct-quick-info__box--proficiency"
),
content: (
<GuidedTourStep
title="Proficiency Bonus"
content={
<>
Proficiency is added to rolls made to accomplish tasks with which
your character is proficient. The bonus is automatically added to
rolls when it is needed. Click here to learn more about how
Proficiency Bonuses are used.
</>
}
/>
),
},
{
selector: getSelector(
".ct-combat-mobile__extra--speed",
".ct-combat-tablet__extra--speed",
".ct-quick-info__box--speed"
),
content: (
<GuidedTourStep
title="Speed"
content={
<>
In each round of combat, your character can move up to the total
distance indicated by your speed. Click here to learn more or
customize your speed.
</>
}
/>
),
},
{
selector: getSelector(
".ct-status-summary-mobile__health",
".ct-status-summary-mobile__health",
".ct-health-summary"
),
content: (
<GuidedTourStep
title="Hit Points"
content={
<>
In this area, you can manage your character's hit points. When
your character reaches zero hit points, they are on the brink of
death and begin making death saving throws.
</>
}
/>
),
},
{
selector: getSelector(
".ct-combat-mobile__extra--initiative",
".ct-combat-tablet__extra--initiative",
".ct-combat__summary-group--initiative"
),
content: (
<GuidedTourStep
title="Initiative"
content={
<>
Initiative rolls determine the order in which you go in combat.
The higher the number, the sooner you get to fight. Click here to
roll initiative!
</>
}
/>
),
},
{
selector: getSelector(
".ct-combat-mobile__extra--ac",
".ct-combat-tablet__extra--ac",
".ct-combat__summary-group--ac"
),
content: (
<GuidedTourStep
title="Armor Class"
content={
<>
Your Armor Class (AC) represents how well your character avoids
being wounded in battle.
</>
}
/>
),
},
{
selector: getSelector(
".ct-main-mobile__saving-throws",
".ct-saving-throws-box",
".ct-saving-throws-box"
),
content: (
<GuidedTourStep
title="Saving Throws"
content={
<>
Your DM may ask you to roll a saving throw (or make a save) to
resist an incoming effect. Click on a save to automatically roll.
</>
}
/>
),
},
...(isDesktop || isTablet
? [
{
selector: ".ct-proficiency-groups-box",
content: (
<GuidedTourStep
title="Proficiencies"
content={
<>
Proficiencies tell you what tools, equipment, and languages
your character is skilled at using.
</>
}
/>
),
},
{
selector: ".ct-skills-box",
content: (
<GuidedTourStep
title="Skills"
content={
<>
If you wish to perform an action, your DM will determine
which of these skills you will use. Click on a skill to roll
or learn more.
</>
}
/>
),
},
]
: [
{
selector: ".ct-quick-nav__menu-item--skills",
content: (
<GuidedTourStep
title="Skills"
content={
<>
If you wish to perform an action, your DM will determine
which of these skills you will use. Click on a skill to roll
or learn more.
</>
}
/>
),
},
]),
...(!isDesktop
? [
{
selector: ".ct-quick-nav__menu-item--actions",
content: (
<GuidedTourStep
title="Actions"
content={
<>
When you take your action on your turn, you can take one of
the actions presented here. You can track actions or make
rolls to perform them by clicking in this panel.
</>
}
/>
),
},
{
selector: ".ct-quick-nav__menu-item--equipment",
content: (
<GuidedTourStep
title="Inventory"
content={
<>
View and manage your character's items and coin from this
panel.
</>
}
/>
),
},
{
selector: ".ct-quick-nav__menu-item--spells",
content: (
<GuidedTourStep
title="Spells"
content={
<>
If your character has the ability to cast spells, look here
for a list of spells and to track your spellcasting.
</>
}
/>
),
},
{
selector: ".ct-quick-nav__menu-item--features",
content: (
<GuidedTourStep
title="Features & Traits"
content={
<>
This section describes the source of your character's
abilities. How this manifests in your character's
personality is up to you.
</>
}
/>
),
},
...(!isTablet
? [
{
selector: ".ct-quick-nav__menu-item--proficiencies",
content: (
<GuidedTourStep
title="Proficiencies"
content={
<>
Proficiencies tell you what tools, equipment, and
languages your character is skilled at using.
</>
}
/>
),
},
]
: []),
{
selector: ".ct-quick-nav__menu-item--description",
content: (
<GuidedTourStep
title="Description"
content={
<>
Use the description panel to tell your character's story.
Your character's background features are also found here.
</>
}
showClose
/>
),
},
]
: [
{
selector: ".ct-primary-box__tab--actions",
content: (
<GuidedTourStep
title="Actions"
content={
<>
When you take your action on your turn, you can take one of
the actions presented here. You can track actions or make
rolls to perform them by clicking in this panel.
</>
}
/>
),
},
...(hasSpells
? [
{
selector: ".ct-primary-box__tab--spells",
content: (
<GuidedTourStep
title="Spells"
content={
<>
If your character has the ability to cast spells, look
here for a list of spells and to track your
spellcasting.
</>
}
/>
),
},
]
: []),
{
selector: ".ct-primary-box__tab--equipment",
content: (
<GuidedTourStep
title="Inventory"
content={
<>
View and manage your character's items and coin from this
panel.
</>
}
/>
),
},
{
selector: ".ct-primary-box__tab--features",
content: (
<GuidedTourStep
title="Features & Traits"
content={
<>
This section describes the source of your character's
abilities. How this manifests in your character's
personality is up to you.
</>
}
/>
),
},
{
selector: ".ct-primary-box__tab--description",
content: (
<GuidedTourStep
title="Description"
content={
<>
Use the description panel to tell your character's story.
Your character's background features are also found here.
</>
}
showClose
/>
),
},
]),
];
return stepList.filter((s) => s);
};
@@ -1,4 +0,0 @@
import CharacterSheetGuidedTour from "./CharacterSheetGuidedTour";
export default CharacterSheetGuidedTour;
export { CharacterSheetGuidedTour };
@@ -130,6 +130,7 @@ class CharacterSheetMobile extends React.PureComponent<Props, State> {
IconComponent: isDarkMode ? LightAbilitiesSvg : DarkAbilitiesSvg,
}}
ContentComponent={MainMobile}
isEnabled
/>
<ComponentCarouselItem
itemKey="skills"
@@ -139,6 +140,7 @@ class CharacterSheetMobile extends React.PureComponent<Props, State> {
IconComponent: isDarkMode ? LightSkillsSvg : DarkSkillsSvg,
}}
ContentComponent={SkillsMobile}
isEnabled
/>
<ComponentCarouselItem
itemKey="actions"
@@ -148,6 +150,7 @@ class CharacterSheetMobile extends React.PureComponent<Props, State> {
IconComponent: isDarkMode ? LightActionsSvg : DarkActionsSvg,
}}
ContentComponent={ActionsMobile}
isEnabled
/>
<ComponentCarouselItem
itemKey="equipment"
@@ -157,6 +160,7 @@ class CharacterSheetMobile extends React.PureComponent<Props, State> {
IconComponent: isDarkMode ? LightEquipmentSvg : DarkEquipmentSvg,
}}
ContentComponent={EquipmentMobile}
isEnabled
/>
<ComponentCarouselItem
itemKey="spells"
@@ -176,6 +180,7 @@ class CharacterSheetMobile extends React.PureComponent<Props, State> {
IconComponent: isDarkMode ? LightFeaturesSvg : DarkFeaturesSvg,
}}
ContentComponent={FeaturesMobile}
isEnabled
/>
<ComponentCarouselItem
itemKey="proficiencies"
@@ -187,6 +192,7 @@ class CharacterSheetMobile extends React.PureComponent<Props, State> {
: DarkProficienciesSvg,
}}
ContentComponent={ProficiencyGroupsMobile}
isEnabled
/>
<ComponentCarouselItem
itemKey="description"
@@ -242,7 +248,7 @@ class CharacterSheetMobile extends React.PureComponent<Props, State> {
]}
>
{(interpolatedStyles) => (
<React.Fragment>
<>
{interpolatedStyles.map((config) => (
<Sidebar
key={config.key}
@@ -255,7 +261,7 @@ class CharacterSheetMobile extends React.PureComponent<Props, State> {
}
/>
))}
</React.Fragment>
</>
)}
</TransitionMotion>
);
@@ -43,7 +43,7 @@ import {
} from "../../components/ComponentCarousel";
import SectionPlaceholder from "../../components/SectionPlaceholder";
import { SheetAppState } from "../../typings";
import BackdropStyles from "../BackdropStyles";
import { BackdropStyles } from "../BackdropStyles";
import CharacterHeaderTablet from "../CharacterHeaderTablet";
import ActionsTablet from "../tablet/ActionsTablet";
import CombatTablet from "../tablet/CombatTablet";
@@ -125,6 +125,7 @@ class CharacterSheetTablet extends React.PureComponent<Props, State> {
IconComponent: isDarkMode ? LightAbilitiesSvg : DarkAbilitiesSvg,
}}
ContentComponent={MainTablet}
isEnabled
/>
<ComponentCarouselItem
itemKey="actions"
@@ -134,6 +135,7 @@ class CharacterSheetTablet extends React.PureComponent<Props, State> {
IconComponent: isDarkMode ? LightActionsSvg : DarkActionsSvg,
}}
ContentComponent={ActionsTablet}
isEnabled
/>
<ComponentCarouselItem
itemKey="equipment"
@@ -143,6 +145,7 @@ class CharacterSheetTablet extends React.PureComponent<Props, State> {
IconComponent: isDarkMode ? LightEquipmentSvg : DarkEquipmentSvg,
}}
ContentComponent={EquipmentTablet}
isEnabled
/>
<ComponentCarouselItem
itemKey="spells"
@@ -162,6 +165,7 @@ class CharacterSheetTablet extends React.PureComponent<Props, State> {
IconComponent: isDarkMode ? LightFeaturesSvg : DarkFeaturesSvg,
}}
ContentComponent={FeaturesTablet}
isEnabled
/>
<ComponentCarouselItem
itemKey="description"
@@ -218,7 +222,7 @@ class CharacterSheetTablet extends React.PureComponent<Props, State> {
]}
>
{(interpolatedStyles) => (
<React.Fragment>
<>
{interpolatedStyles.map((config) => (
<Sidebar
key={config.key}
@@ -231,7 +235,7 @@ class CharacterSheetTablet extends React.PureComponent<Props, State> {
}
/>
))}
</React.Fragment>
</>
)}
</TransitionMotion>
);
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect } from "react-redux";
@@ -19,6 +18,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { InitiativeBox } from "~/subApps/sheet/components/InitiativeBox";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
@@ -120,7 +120,9 @@ class Combat extends React.PureComponent<Props> {
StyleComponent={BoxBackgroundComponent}
theme={theme}
/>
<h2 style={visuallyHidden}>Defenses and Conditions</h2>
<h2 className={a11yStyles.screenreaderOnly}>
Defenses and Conditions
</h2>
<div
className="ct-combat__statuses-group ct-combat__statuses-group--defenses"
onClick={this.handleDefensesSummaryClick}
@@ -1,7 +1,7 @@
import { visuallyHidden } from "@mui/utils";
import clsx from "clsx";
import React, { useContext } from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
AlignmentContract,
@@ -28,6 +28,7 @@ import { NumberDisplay } from "~/components/NumberDisplay";
import { TabFilter } from "~/components/TabFilter";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { CharacterFeaturesManagerContext } from "~/tools/js/Shared/managers/CharacterFeaturesManagerContext";
@@ -45,7 +46,9 @@ import styles from "./styles.module.css";
const DEFAULT_VALUE = "--";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
isVertical: boolean;
background: Background | null;
alignment: AlignmentContract | null;
@@ -187,14 +190,9 @@ class Description extends React.PureComponent<Props> {
traits,
} = this.props;
const infoItemProps = {
role: "listitem",
inline: isVertical,
};
return (
<section className="ct-description">
<h2 style={visuallyHidden}>Description</h2>
<h2 className={a11yStyles.screenreaderOnly}>Description</h2>
<TabFilter
filters={[
{
@@ -240,34 +238,34 @@ class Description extends React.PureComponent<Props> {
)
}
>
<InfoItem label="Alignment" {...infoItemProps}>
<InfoItem label="Alignment" inline={isVertical}>
{alignment === null ? DEFAULT_VALUE : alignment.name}
</InfoItem>
<InfoItem label="Gender" {...infoItemProps}>
<InfoItem label="Gender" inline={isVertical}>
{this.renderDescriptionItem(gender)}
</InfoItem>
<InfoItem label="Eyes" {...infoItemProps}>
<InfoItem label="Eyes" inline={isVertical}>
{this.renderDescriptionItem(eyes)}
</InfoItem>
<InfoItem label="Size" {...infoItemProps}>
<InfoItem label="Size" inline={isVertical}>
{this.renderDescriptionItem(size ? size.name : null)}
</InfoItem>
<InfoItem label="Height" {...infoItemProps}>
<InfoItem label="Height" inline={isVertical}>
{this.renderDescriptionItem(height)}
</InfoItem>
<InfoItem label="Faith" {...infoItemProps}>
<InfoItem label="Faith" inline={isVertical}>
{this.renderDescriptionItem(faith)}
</InfoItem>
<InfoItem label="Hair" {...infoItemProps}>
<InfoItem label="Hair" inline={isVertical}>
{this.renderDescriptionItem(hair)}
</InfoItem>
<InfoItem label="Skin" {...infoItemProps}>
<InfoItem label="Skin" inline={isVertical}>
{this.renderDescriptionItem(skin)}
</InfoItem>
<InfoItem label="Age" {...infoItemProps}>
<InfoItem label="Age" inline={isVertical}>
{this.renderDescriptionItem(age)}
</InfoItem>
<InfoItem label="Weight" {...infoItemProps}>
<InfoItem label="Weight" inline={isVertical}>
{weight === null ? (
DEFAULT_VALUE
) : (
@@ -1,25 +1,10 @@
import Alert from "@mui/material/Alert";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import Stack from "@mui/material/Stack";
import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs";
import Typography from "@mui/material/Typography";
import { visuallyHidden } from "@mui/utils";
import { useContext, ReactNode, MouseEvent, PureComponent } from "react";
import clsx from "clsx";
import { useContext, ReactNode, PureComponent } from "react";
import React from "react";
import { connect } from "react-redux";
import { DarkBuilderSvg } from "@dndbeyond/character-components/es";
import {
DarkBuilderSvg,
FeatureFlagContext,
} from "@dndbeyond/character-components/es";
import {
CharacterCurrencyContract,
CharacterNotes,
CharacterTheme,
Constants,
@@ -33,23 +18,29 @@ import {
SnippetData,
RuleData,
rulesEngineSelectors,
characterSelectors,
BaseCharClass,
ContainerUtils,
InventoryManager,
FormatUtils,
CampaignUtils,
serviceDataSelectors,
PartyInfo,
CoinManager,
ClassUtils,
CharClass,
ItemPlan,
ItemPlanUtils,
} from "@dndbeyond/character-rules-engine/es";
import { Button } from "~/components/Button";
import { ItemName } from "~/components/ItemName";
import { Link } from "~/components/Link";
import { NumberDisplay } from "~/components/NumberDisplay";
import { TabFilter } from "~/components/TabFilter";
import { TabList } from "~/components/TabList";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { EquipmentOverview } from "~/subApps/sheet/components/Equipment/EquipmentOverview";
import { MagicItemPlans } from "~/subApps/sheet/components/MagicItemPlans";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import CurrencyButton from "../../../CharacterSheet/components/CurrencyButton";
@@ -60,7 +51,6 @@ import { InventoryManagerContext } from "../../../Shared/managers/InventoryManag
import { appEnvSelectors } from "../../../Shared/selectors";
import { PaneIdentifierUtils } from "../../../Shared/utils";
import ContentGroup from "../../components/ContentGroup";
import EquipmentOverview from "../../components/EquipmentOverview";
import Infusions from "../../components/Infusions";
import InventoryFilter from "../../components/InventoryFilter";
import InventoryTableHeader from "../../components/InventoryTableHeader";
@@ -69,6 +59,7 @@ import { sheetAppSelectors } from "../../selectors";
import { SheetAppState } from "../../typings";
import Attunement from "../Attunement";
import Inventory from "../Inventory";
import styles from "./styles.module.css";
interface Props {
showNotes: boolean;
@@ -77,22 +68,21 @@ interface Props {
inventory: Array<Item>;
partyInventory: Array<Item>;
creatures: Array<Creature>;
currencies: CharacterCurrencyContract | null;
notes: CharacterNotes;
infusionChoices: Array<InfusionChoice>;
weight: number;
weightSpeedType: Constants.WeightSpeedTypeEnum;
ruleData: RuleData;
snippetData: SnippetData;
isReadonly: boolean;
isMobile: boolean;
theme: CharacterTheme;
proficiencyBonus: number;
classes: Array<BaseCharClass>;
classes: Array<CharClass>;
inventoryManager: InventoryManager;
campaignInfo: PartyInfo | null;
coinManager: CoinManager;
paneHistoryStart: PaneInfo["paneHistoryStart"];
itemPlans: Array<ItemPlan>;
maxReplicatedItemsCount: number | null;
}
interface StateFilterData {
filteredInventory: Array<Item>;
@@ -105,7 +95,6 @@ interface State {
filterData: StateFilterData;
filteredEquippedInventory: Array<Item>;
shouldShowPartyInventory: number;
shouldShowDeleteOnlyInfo: boolean;
}
class Equipment extends PureComponent<Props, State> {
@@ -128,7 +117,6 @@ class Equipment extends PureComponent<Props, State> {
},
filteredEquippedInventory: [],
shouldShowPartyInventory: 0, // mui used 0,1,2 for tabs so 0 is off 1 is on...
shouldShowDeleteOnlyInfo: false,
};
}
@@ -157,16 +145,6 @@ class Equipment extends PureComponent<Props, State> {
);
};
handleWeightClick = (): void => {
const { paneHistoryStart } = this.props;
paneHistoryStart(PaneComponentEnum.ENCUMBRANCE);
};
handleCampaignClick = (): void => {
const { paneHistoryStart } = this.props;
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
};
handlePossessionsManage = (): void => {
const { paneHistoryStart, isReadonly } = this.props;
@@ -233,12 +211,26 @@ class Equipment extends PureComponent<Props, State> {
);
};
handleItemPlansManagePaneShow = (): void => {
const { paneHistoryStart } = this.props;
paneHistoryStart(PaneComponentEnum.ITEM_PLANS_MANAGE);
};
shouldRenderInfusions = (): boolean => {
const { infusionChoices } = this.props;
return infusionChoices.length > 0;
};
shouldRenderMagicItemPlans = (): boolean => {
const { classes } = this.props;
return classes.some((charClass) =>
ClassUtils.getEnablesReplicateMagicItem(charClass)
);
};
renderContainer = (
container: ContainerManager,
inventory: Array<Item>
@@ -380,7 +372,7 @@ class Equipment extends PureComponent<Props, State> {
};
renderAttunement = (): ReactNode => {
const { isMobile, theme } = this.props;
const { isMobile } = this.props;
return (
<ContentGroup header="Attunement">
@@ -389,6 +381,54 @@ class Equipment extends PureComponent<Props, State> {
);
};
renderMagicItemPlans = (): ReactNode => {
const { isReadonly, itemPlans, maxReplicatedItemsCount } = this.props;
if (!this.shouldRenderMagicItemPlans()) {
return null;
}
const itemPlanWithMappingsCount = itemPlans.filter((plan) =>
ItemPlanUtils.getItem(plan)
).length;
const headerNode: ReactNode = (
<>
Magic Item Plans{" "}
{maxReplicatedItemsCount !== null && (
<span className={styles.replicatedCount}>
({itemPlanWithMappingsCount}/{maxReplicatedItemsCount} Replicated)
</span>
)}
</>
);
let extraNode: ReactNode = null;
if (!isReadonly) {
extraNode = (
<Button
variant="text"
themed
size="x-small"
className="ct-equipment__builder-link-text"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
this.handleItemPlansManagePaneShow();
}}
>
Manage Plans
</Button>
);
}
return (
<ContentGroup header={headerNode} extra={extraNode}>
<MagicItemPlans />
</ContentGroup>
);
};
renderInfusions = (): ReactNode => {
const { infusableChoices } = this.state;
const {
@@ -421,7 +461,7 @@ class Equipment extends PureComponent<Props, State> {
<Link
href={builderUrl}
className="ct-equipment__builder-link"
useRouter
userouter
>
<DarkBuilderSvg />
<span className="ct-equipment__builder-link-text">
@@ -490,7 +530,7 @@ class Equipment extends PureComponent<Props, State> {
);
return (
<div className="ct-equipment__content">
<div className={styles.tabFilter}>
<TabFilter
sharedChildren={<InventoryTableHeader showNotes={showNotes} />}
filters={[
@@ -503,20 +543,16 @@ class Equipment extends PureComponent<Props, State> {
{this.renderCharacterContainers()}
{this.renderAttunement()}
{this.shouldRenderInfusions() && this.renderInfusions()}
{this.renderMagicItemPlans()}
{this.renderOtherPossessions()}
</>
) : null}
<FeatureFlagContext.Consumer>
{({ imsFlag }) => {
return imsFlag &&
shouldShowPartyInventory === 1 &&
inventoryManager.getPartyContainers().length > 0 ? (
<ContentGroup header={headerNode}>
{this.renderPartyContainers()}
</ContentGroup>
) : null;
}}
</FeatureFlagContext.Consumer>
{shouldShowPartyInventory === 1 &&
inventoryManager.getPartyContainers().length > 0 ? (
<ContentGroup header={headerNode}>
{this.renderPartyContainers()}
</ContentGroup>
) : null}
</>
),
},
@@ -551,6 +587,14 @@ class Equipment extends PureComponent<Props, State> {
},
]
: []),
...(this.shouldRenderMagicItemPlans()
? [
{
label: "Magic Item Plans",
content: this.renderMagicItemPlans(),
},
]
: []),
...(!isReadonly
? [
{
@@ -592,126 +636,17 @@ class Equipment extends PureComponent<Props, State> {
shouldShowPartyInventory: newValue,
});
};
handleCloseDeleteOnlyInfo = (event: MouseEvent): void => {
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
this.setState({
shouldShowDeleteOnlyInfo: false,
});
};
handleOpenDeleteOnlyInfo = (event: MouseEvent): void => {
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
this.setState({
shouldShowDeleteOnlyInfo: true,
});
};
renderDeleteOnlyAlertAction = (): ReactNode => {
const { shouldShowDeleteOnlyInfo } = this.state;
const { campaignInfo } = this.props;
return (
<div>
<ThemeButton
style="outline"
size="medium"
onClick={this.handleOpenDeleteOnlyInfo}
>
More Info
</ThemeButton>
<Dialog
open={shouldShowDeleteOnlyInfo}
onClose={this.handleCloseDeleteOnlyInfo}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Only</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Party Inventory sharing isnt currently enabled for this campaign.
Your old items are still here, but you wont be able to add new
items until its turned on again.
</DialogContentText>
</DialogContent>
<DialogActions>
{campaignInfo ? (
<Button
variant="outlined"
color="primary"
component="a"
href={CampaignUtils.getLink(campaignInfo) || "/my-campaigns"}
size="small"
>
go to campaign
</Button>
) : null}
<Button
variant="outlined"
color="primary"
onClick={this.handleCloseDeleteOnlyInfo}
autoFocus
size="small"
>
Close
</Button>
</DialogActions>
</Dialog>
</div>
);
};
renderOverviewFiltersAndContent = (): ReactNode => {
const { filterData, shouldShowPartyInventory } = this.state;
const {
currencies,
weight,
weightSpeedType,
inventory,
ruleData,
theme,
partyInventory,
campaignInfo,
inventoryManager,
coinManager,
} = this.props;
const cointainerFlagEnabled: boolean = coinManager.canUseCointainers();
let coin: CharacterCurrencyContract | null = currencies;
if (shouldShowPartyInventory === 1 && campaignInfo) {
// We need all the party coin if flag is on, or just Party Equipment coin if not
if (cointainerFlagEnabled) {
coin = coinManager.getAllPartyCoin();
} else {
coin = CampaignUtils.getCoin(campaignInfo);
}
} else if (cointainerFlagEnabled) {
// We need to change it to all container currencies
coin = coinManager.getAllCharacterCoin();
}
const { inventory, ruleData, theme, partyInventory } = this.props;
return (
<>
<div className="ct-equipment__overview">
<EquipmentOverview
shouldShowPartyInventory={shouldShowPartyInventory === 1}
currencies={coin}
weight={weight}
weightSpeedType={weightSpeedType}
onCurrencyClick={(evt) =>
this.handleCurrencyClick(
evt,
shouldShowPartyInventory === 1
? inventoryManager.getPartyEquipmentContainerDefinitionKey() ??
inventoryManager.getCharacterContainerDefinitionKey()
: inventoryManager.getCharacterContainerDefinitionKey()
)
}
onWeightClick={this.handleWeightClick}
onCampaignClick={this.handleCampaignClick}
enableManage={false}
isDarkMode={theme.isDarkMode}
campaignName={
campaignInfo !== null ? CampaignUtils.getName(campaignInfo) : null
activeEquipmentTab={
shouldShowPartyInventory === 1 ? "party" : "character"
}
/>
</div>
@@ -730,113 +665,39 @@ class Equipment extends PureComponent<Props, State> {
);
};
renderOptinToInventorySharing = (): ReactNode => {
handleChangeTab = (id) =>
this.setState({
shouldShowPartyInventory: id === "party-inventory" ? 1 : 0,
});
render() {
const { campaignInfo } = this.props;
return (
<>
<Stack justifyContent="center">
<Stack marginBottom="10px" marginTop="40px">
<Typography color="primary" align="center" variant="body2">
Want a hand with that loot?
</Typography>
</Stack>
<Stack marginBottom="30px" alignItems={"center"}>
<Typography
align="center"
variant="body1"
sx={{ maxWidth: "320px" }}
>
Party Inventory makes it easy for your group to keep track of all
their shared loot and coin. Add it to your campaign today with a
subscription to D&D Beyond!
</Typography>
</Stack>
<Stack
spacing={2}
direction={{ xs: "column", sm: "row" }}
justifyContent="center"
>
{campaignInfo ? (
<Button
variant="contained"
color="primary"
component="a"
href={CampaignUtils.getLink(campaignInfo) || "/my-campaigns"}
size="large"
sx={{
marginLeft: { xs: 0 },
marginRight: { xs: 0 },
}}
>
go to campaign
</Button>
) : null}
<Button variant="contained" color="primary" size="large">
view subscription plans
</Button>
</Stack>
<Stack
style={{ display: "flex", justifyContent: "center", marginTop: 51 }}
>
<div className="ct-item-detail__full-image">
<img
className="ct-item-detail__full-image-img"
src="https://www.dndbeyond.com/avatars/thumbnails/7/120/315/315/636284708068284913.jpeg"
alt=""
/>
</div>
</Stack>
</Stack>
</>
);
};
render() {
const { shouldShowPartyInventory } = this.state;
const { campaignInfo, inventoryManager } = this.props;
return (
<section className="ct-equipment">
<h2 style={visuallyHidden}>Inventory</h2>
<FeatureFlagContext.Consumer>
{({ imsFlag }) => {
return imsFlag && campaignInfo !== null ? (
<>
<Tabs
value={shouldShowPartyInventory}
indicatorColor="primary"
textColor="primary"
onChange={this.toggleShouldShowPartyInventory}
aria-label="disabled tabs example"
variant="fullWidth"
>
<Tab label="My Inventory" data-testid="my-inventory" />
<Tab label="Party Inventory" data-testid="party-inventory" />
</Tabs>
{/* TODO: make this go away when last itme is removed... */}
{shouldShowPartyInventory &&
inventoryManager.isSharingTurnedDeleteOnly() ? (
<div role="button" onMouseUp={this.handleOpenDeleteOnlyInfo}>
<Alert
severity="info"
action={this.renderDeleteOnlyAlertAction()}
>
Party Inventory is currently turned off for this campaign.
</Alert>
</div>
) : null}
{shouldShowPartyInventory &&
inventoryManager.isSharingTurnedOff()
? this.renderOptinToInventorySharing()
: this.renderOverviewFiltersAndContent()}
</>
) : (
this.renderOverviewFiltersAndContent()
);
}}
</FeatureFlagContext.Consumer>
<section className={clsx(["ct-equipment", styles.section])}>
<h2 className={a11yStyles.screenreaderOnly}>Inventory</h2>
{campaignInfo !== null ? (
<TabList
className={styles.tabList}
variant="fullwidth"
themed={true}
tabs={[
{
label: "My Inventory",
content: this.renderOverviewFiltersAndContent(),
id: "my-inventory",
},
{
label: "Party Inventory",
content: this.renderOverviewFiltersAndContent(),
id: "party-inventory",
},
]}
onChangeTab={this.handleChangeTab}
/>
) : (
this.renderOverviewFiltersAndContent()
)}
</section>
);
}
@@ -848,19 +709,19 @@ function mapStateToProps(state: SheetAppState) {
inventory: rulesEngineSelectors.getInventory(state),
partyInventory: rulesEngineSelectors.getPartyInventory(state),
creatures: rulesEngineSelectors.getCreatures(state),
weight: rulesEngineSelectors.getTotalCarriedWeight(state),
weightSpeedType: rulesEngineSelectors.getCurrentCarriedWeightType(state),
notes: rulesEngineSelectors.getCharacterNotes(state),
infusionChoices: rulesEngineSelectors.getAvailableInfusionChoices(state),
currencies: rulesEngineSelectors.getCurrencies(state),
ruleData: rulesEngineSelectors.getRuleData(state),
snippetData: rulesEngineSelectors.getSnippetData(state),
theme: rulesEngineSelectors.getCharacterTheme(state),
isReadonly: appEnvSelectors.getIsReadonly(state),
isMobile: appEnvSelectors.getIsMobile(state),
proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state),
classes: characterSelectors.getClasses(state),
classes: rulesEngineSelectors.getClasses(state),
campaignInfo: serviceDataSelectors.getPartyInfo(state),
itemPlans: rulesEngineSelectors.getAvailableItemPlans(state),
maxReplicatedItemsCount:
rulesEngineSelectors.getMaxReplicatedItemsCount(state),
};
}
@@ -1,6 +1,6 @@
import { visuallyHidden } from "@mui/utils";
import React, { useContext } from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
rulesEngineSelectors,
@@ -16,6 +16,7 @@ import { TabFilter } from "~/components/TabFilter";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import { useExtras } from "~/hooks/useExtras";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import ExtraList from "../../../Shared/components/ExtraList";
@@ -27,7 +28,9 @@ import ContentGroup from "../../components/ContentGroup";
import ExtrasFilter from "../../components/ExtrasFilter";
import { SheetAppState } from "../../typings";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
extras: Array<ExtraManager>;
extrasManager: ExtrasManager;
isReadonly: boolean;
@@ -229,7 +232,7 @@ class Extras extends React.PureComponent<Props, State> {
theme.isDarkMode ? "ct-extras--dark-mode" : ""
}`}
>
<h2 style={visuallyHidden}>Extras</h2>
<h2 className={a11yStyles.screenreaderOnly}>Extras</h2>
<div className="ct-extras__filter">
<ExtrasFilter
extras={extras}
@@ -1,6 +1,5 @@
import { visuallyHidden } from "@mui/utils";
import React, { useContext, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import React, { useContext } from "react";
import { useSelector } from "react-redux";
import {
rulesEngineSelectors,
@@ -9,7 +8,6 @@ import {
InfusionChoiceUtils,
RacialTrait,
RacialTraitUtils,
FeaturesManager,
FeatManager,
Action,
ActionUtils,
@@ -18,8 +16,9 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { TabFilter } from "~/components/TabFilter";
import { useFeatureFlags } from "~/contexts/FeatureFlag";
import { useSidebar } from "~/contexts/Sidebar";
import { useDispatch } from "~/hooks/useDispatch";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { SpeciesDetail } from "~/subApps/sheet/components/SpeciesDetail";
import { ThemeButton } from "~/tools/js/Shared/components/common/Button";
@@ -31,7 +30,6 @@ import {
import { CharacterFeaturesManagerContext } from "../../../Shared/managers/CharacterFeaturesManagerContext";
import { appEnvSelectors } from "../../../Shared/selectors";
import { PaneIdentifierUtils } from "../../../Shared/utils";
import BlessingsDetail from "../../components/BlessingsDetail";
import ClassesDetail from "../../components/ClassesDetail";
import ContentGroup from "../../components/ContentGroup";
import FeatsDetail from "../../components/FeatsDetail";
@@ -230,7 +228,6 @@ const ClassFeaturesGroup: React.FC<{}> = () => {
const FeatsGroup: React.FC<{}> = () => {
const dispatch = useDispatch();
const { gfsBlessingsUiFlag } = useFeatureFlags();
const {
pane: { paneHistoryStart },
} = useSidebar();
@@ -285,7 +282,7 @@ const FeatsGroup: React.FC<{}> = () => {
style="outline"
size="medium"
>
Manage {gfsBlessingsUiFlag ? "Features" : "Feats"}
Manage Feats
</ThemeButton>
</div>
)}
@@ -311,35 +308,10 @@ const FeatsGroup: React.FC<{}> = () => {
);
};
const BlessingsGroup: React.FC<{}> = () => {
return (
<ContentGroup header="Blessings">
<BlessingsDetail />
</ContentGroup>
);
};
const Features: React.FC<{}> = () => {
const { gfsBlessingsUiFlag } = useFeatureFlags();
const { characterFeaturesManager } = useContext(
CharacterFeaturesManagerContext
);
const [hasBlessings, setHasBlessings] = useState(false);
useEffect(() => {
async function onUpdate() {
const hasBlessings = gfsBlessingsUiFlag
? await characterFeaturesManager.hasBlessings()
: false;
setHasBlessings(hasBlessings);
}
return FeaturesManager.subscribeToUpdates({ onUpdate });
}, [setHasBlessings]);
return (
<section className="ct-features">
<h2 style={visuallyHidden}>Features and Traits</h2>
<h2 className={a11yStyles.screenreaderOnly}>Features and Traits</h2>
<TabFilter
filters={[
{
@@ -354,9 +326,6 @@ const Features: React.FC<{}> = () => {
label: "Feats",
content: <FeatsGroup />,
},
...(gfsBlessingsUiFlag && hasBlessings
? [{ label: "Blessings", content: <BlessingsGroup /> }]
: []),
]}
/>
</section>
@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useState } from "react";
import React, { useCallback, useState } from "react";
import {
CharacterTheme,
@@ -7,10 +7,10 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { useSidebar } from "~/contexts/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { ThemeButton } from "../../../Shared/components/common/Button";
import { InventoryManagerContext } from "../../../Shared/managers/InventoryManagerContext";
import { PaneIdentifierUtils } from "../../../Shared/utils";
import InventoryItem from "../../components/InventoryItem";
import { InventoryTableHeader } from "../../components/InventoryTableHeader";
@@ -42,8 +42,6 @@ const InventoryActions: React.FC<InventoryActionsProps> = ({
onShowContentsClick,
showContents,
}) => {
const { inventoryManager } = useContext(InventoryManagerContext);
if (isReadonly) {
return null;
}
@@ -57,24 +55,20 @@ const InventoryActions: React.FC<InventoryActionsProps> = ({
return (
<div className={classNames.join(" ")}>
{inventoryManager.canAddToContainer(container.container) ? (
<span
role="button"
className="ct-inventory__action"
onClick={(evt) => onContainerClick(evt, container, true)}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
onContainerClick(evt, container, true);
}
}}
>
{container.isCharacterContainer()
? `+ Add ${containerName}`
: `+ Add items to your ${containerName}`}
</span>
) : (
<span />
)}
<span
role="button"
className="ct-inventory__action"
onClick={(evt) => onContainerClick(evt, container, true)}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
onContainerClick(evt, container, true);
}
}}
>
{container.isCharacterContainer()
? `+ Add ${containerName}`
: `+ Add items to your ${containerName}`}
</span>
{inventory.length > 0 && (
<span
role="button"
@@ -192,7 +186,7 @@ const Inventory: React.FC<Props> = ({
return (
<div className="ct-inventory">
{/*<h2 style={visuallyHidden}>Inventory</h2>*/}
<h2 className={a11yStyles.screenreaderOnly}>Inventory</h2>
{showTableHeader && <InventoryTableHeader showNotes={showNotes} />}
<div className="ct-inventory__items">
{isEmpty ? (
@@ -205,7 +199,7 @@ const Inventory: React.FC<Props> = ({
onContainerClick={handleContainerShow}
/>
) : (
<React.Fragment>
<>
{showContents &&
inventory.map((item) => (
<InventoryItem
@@ -232,7 +226,7 @@ const Inventory: React.FC<Props> = ({
onShowContentsClick={setShowContents}
showContents={showContents}
/>
</React.Fragment>
</>
)}
</div>
</div>
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect } from "react-redux";
@@ -10,6 +9,7 @@ import {
import { TabFilter } from "~/components/TabFilter";
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { appEnvSelectors } from "../../../Shared/selectors";
@@ -71,7 +71,7 @@ class Notes extends React.PureComponent<NotesProps> {
render() {
return (
<section className="ct-notes">
<h2 style={visuallyHidden}>Notes</h2>
<h2 className={a11yStyles.screenreaderOnly}>Notes</h2>
<TabFilter
filters={[
{
@@ -1,5 +1,7 @@
import clsx from "clsx";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
BeveledBoxSvg517x660,
@@ -39,7 +41,9 @@ const TAB_KEY = {
EXTRAS: "EXTRAS",
};
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
hasSpells: boolean;
dimensions: AppEnvDimensionsState;
theme: CharacterTheme;
@@ -62,9 +66,11 @@ class PrimaryBox extends React.PureComponent<Props, {}> {
return (
<Subsection name="Primary Box">
<div
className={`ct-primary-box ${
theme.isDarkMode ? "ct-primary-box--dark-mode" : ""
}`}
className={clsx([
"ct-primary-box",
theme.isDarkMode && "ct-primary-box--dark-mode",
styles.primaryBox,
])}
>
<BoxBackground
StyleComponent={BoxBackgroundComponent}
@@ -122,6 +128,7 @@ class PrimaryBox extends React.PureComponent<Props, {}> {
id: TAB_KEY.EXTRAS,
},
]}
themed
/>
</div>
</Subsection>
@@ -1,5 +1,4 @@
import { visuallyHidden } from "@mui/utils";
import { useDispatch, useSelector } from "react-redux";
import { useSelector } from "react-redux";
import { AbilitySummary } from "@dndbeyond/character-components/es";
import {
@@ -10,6 +9,8 @@ import {
import { useSidebar } from "~/contexts/Sidebar";
import { useAbilities } from "~/hooks/useAbilities";
import { useDispatch } from "~/hooks/useDispatch";
import a11yStyles from "~/styles/accessibility.module.css";
import { HitPointsBox } from "~/subApps/sheet/components/HitPointsBox/HitPointsBox";
import { Inspiration } from "~/subApps/sheet/components/Inspiration";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
@@ -76,7 +77,7 @@ export default function QuickInfo() {
return (
<div className="ct-quick-info">
<section className="ct-quick-info__abilities">
<h2 style={visuallyHidden}>Ability Scores</h2>
<h2 className={a11yStyles.screenreaderOnly}>Ability Scores</h2>
{abilities.map((ability) => (
<div className="ct-quick-info__ability" key={ability.getStatKey()}>
<AbilitySummary
@@ -107,7 +108,7 @@ export default function QuickInfo() {
/>
</div>
<section className="ct-quick-info__inspiration">
<h2 style={visuallyHidden}>Inspiration</h2>
<h2 className={a11yStyles.screenreaderOnly}>Inspiration</h2>
<Inspiration
inspiration={!!inspiration}
onToggle={handleToggleInspiration}
@@ -30,10 +30,10 @@ export default function SavingThrowsDesktop() {
const ruleData = useSelector(rulesEngineSelectors.getRuleData);
const savingThrowDiceAdjustments = useSelector(
rulesEngineSelectors.getSavingThrowDiceAdjustments
); // TODO: GFS move to mangers
);
const situationalBonusSavingThrowsLookup = useSelector(
rulesEngineSelectors.getSituationalBonusSavingThrowsLookup
); // TODO: GFS move to mangers
);
const deathSaveInfo = useSelector(rulesEngineSelectors.getDeathSaveInfo);
const dimensions = useSelector(appEnvSelectors.getDimensions);
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
@@ -10,7 +10,7 @@ import {
CharacterTheme,
RuleData,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
@@ -36,7 +36,7 @@ interface Props {
isReadonly: boolean;
diceEnabled: boolean;
ruleData: RuleData;
characterRollContext: IRollContext;
characterRollContext: RollContext;
paneHistoryStart: PaneInfo["paneHistoryStart"];
}
class SkillsDesktop extends React.PureComponent<Props> {
@@ -1,5 +1,6 @@
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
ApiRequestHelpers,
@@ -12,7 +13,9 @@ import { appEnvSelectors } from "../../../Shared/selectors";
import { SheetAppState } from "../../typings";
import SpellSlotManagerGroup from "./SpellSlotManagerGroup";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
spellSlots: Array<SpellSlotContract>;
pactSlots: Array<SpellSlotContract>;
isReadonly: boolean;
@@ -1,6 +1,6 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
ConcentrationIcon,
@@ -28,10 +28,11 @@ import {
SpellSlotContract,
SpellUtils,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import { TabFilter } from "~/components/TabFilter";
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import SpellsLevel from "../../../Shared/components/SpellsLevel";
@@ -70,7 +71,9 @@ function hasFilteredLevelSpells(
}, false);
}
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
levelSpells: Array<Array<ScaledSpell>>;
spellCasterInfo: SpellCasterInfo;
ruleData: RuleData;
@@ -84,7 +87,7 @@ interface Props extends DispatchProp {
diceEnabled: boolean;
dataOriginRefData: DataOriginRefData;
proficiencyBonus: number;
characterRollContext: IRollContext;
characterRollContext: RollContext;
inventoryManager: InventoryManager;
inventoryLookup: InventoryLookup;
partyInventoryLookup: InventoryLookup;
@@ -352,7 +355,7 @@ class Spells extends React.PureComponent<Props, State> {
} = this.props;
return (
<React.Fragment>
<>
{levelSpells.map((spells, level) => {
if (!showLevels.includes(ALL_LEVELS) && !showLevels.includes(level)) {
return null;
@@ -438,7 +441,7 @@ class Spells extends React.PureComponent<Props, State> {
</ContentGroup>
);
})}
</React.Fragment>
</>
);
};
@@ -449,7 +452,7 @@ class Spells extends React.PureComponent<Props, State> {
return (
<section className="ct-spells">
<h2 style={visuallyHidden}>Spells</h2>
<h2 className={a11yStyles.screenreaderOnly}>Spells</h2>
<div className="ct-spells__casting">
<SpellsLevelCasting
spellCasterInfo={spellCasterInfo}
@@ -1,18 +1,22 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
CharacterTheme,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import a11yStyles from "~/styles/accessibility.module.css";
import MobileDivider from "../../../components/MobileDivider";
import SubsectionMobile from "../../../components/SubsectionMobile";
import { SheetAppState } from "../../../typings";
import Actions from "../../Actions";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
theme: CharacterTheme;
}
class ActionsMobile extends React.PureComponent<Props> {
@@ -21,12 +25,12 @@ class ActionsMobile extends React.PureComponent<Props> {
return (
<SubsectionMobile>
<MobileDivider label="Actions" theme={theme} />
<section className="ct-actions-mobile">
<h2 style={visuallyHidden}>Actions</h2>
<MobileDivider label="Actions" theme={theme} />
<h2 className={a11yStyles.screenreaderOnly}>Actions</h2>
<Actions showNotes={false} />
<MobileDivider isEnd={true} theme={theme} />
</section>
<MobileDivider isEnd={true} theme={theme} />
</SubsectionMobile>
);
}
@@ -1,6 +1,7 @@
import clsx from "clsx";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
CharacterPreferences,
@@ -25,7 +26,9 @@ import { appEnvSelectors } from "../../../../Shared/selectors";
import { SheetAppState } from "../../../typings";
import styles from "./styles.module.css";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
armorClass: number;
speeds: SpeedInfo;
proficiencyBonus: number;
@@ -232,6 +235,8 @@ function mapStateToProps(state: SheetAppState) {
};
}
CombatMobile.contextType = GameLogContext;
const CombatMobileContainer = (props) => {
const {
pane: { paneHistoryStart },
@@ -239,6 +244,4 @@ const CombatMobileContainer = (props) => {
return <CombatMobile {...props} paneHistoryStart={paneHistoryStart} />;
};
CombatMobileContainer.contextType = GameLogContext;
export default connect(mapStateToProps)(CombatMobileContainer);
@@ -1,7 +1,7 @@
import { visuallyHidden } from "@mui/utils";
import { FC } from "react";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import a11yStyles from "~/styles/accessibility.module.css";
import MobileDivider from "../../../components/MobileDivider";
import SubsectionMobile from "../../../components/SubsectionMobile";
@@ -13,7 +13,7 @@ export const DescriptionMobile: FC = () => {
<SubsectionMobile className="ct-description-mobile">
<MobileDivider label={"Background"} theme={characterTheme} />
<section>
<h2 style={visuallyHidden}>Background</h2>
<h2 className={a11yStyles.screenreaderOnly}>Background</h2>
<Description theme={characterTheme} isVertical={true} />
</section>
<MobileDivider isEnd={true} theme={characterTheme} />
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect } from "react-redux";
@@ -8,6 +7,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { appEnvSelectors } from "../../../../Shared/selectors";
@@ -42,7 +42,7 @@ class EquipmentMobile extends React.PureComponent<Props> {
theme={theme}
/>
<section className="ct-equipment-mobile">
<h2 style={visuallyHidden}>Inventory</h2>
<h2 className={a11yStyles.screenreaderOnly}>Inventory</h2>
<Equipment showNotes={false} />
</section>
<MobileDivider isEnd={true} theme={theme} />
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect } from "react-redux";
@@ -8,6 +7,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { appEnvSelectors } from "../../../../Shared/selectors";
@@ -42,7 +42,7 @@ class ExtrasMobile extends React.PureComponent<Props> {
theme={theme}
/>
<section className="ct-extras-mobile">
<h2 style={visuallyHidden}>Extras</h2>
<h2 className={a11yStyles.screenreaderOnly}>Extras</h2>
<Extras showNotes={false} />
</section>
<MobileDivider isEnd={true} theme={theme} />
@@ -1,18 +1,22 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
CharacterTheme,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import a11yStyles from "~/styles/accessibility.module.css";
import MobileDivider from "../../../components/MobileDivider";
import SubsectionMobile from "../../../components/SubsectionMobile";
import { SheetAppState } from "../../../typings";
import Features from "../../Features";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
theme: CharacterTheme;
}
class FeaturesMobile extends React.PureComponent<Props> {
@@ -23,7 +27,7 @@ class FeaturesMobile extends React.PureComponent<Props> {
<SubsectionMobile>
<MobileDivider label="Features & Traits" theme={theme} />
<section className="ct-features-mobile">
<h2 style={visuallyHidden}>Features and Traits</h2>
<h2 className={a11yStyles.screenreaderOnly}>Features and Traits</h2>
<Features />
</section>
<MobileDivider isEnd={true} theme={theme} />
@@ -1,10 +1,12 @@
import { visuallyHidden } from "@mui/utils";
import { useSelector } from "react-redux";
import { useSearchParams } from "react-router-dom";
import {
AbilitySummary,
CampaignSummary,
SavingThrowsSummary,
ThemedLongRestSvg,
ThemedShortRestSvg,
} from "@dndbeyond/character-components/es";
import {
AbilityManager,
@@ -12,8 +14,10 @@ import {
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import { Button } from "~/components/Button";
import { useSidebar } from "~/contexts/Sidebar";
import { useAbilities } from "~/hooks/useAbilities";
import a11yStyles from "~/styles/accessibility.module.css";
import { getDataOriginComponentInfo } from "../../../../../../subApps/sheet/components/Sidebar/helpers/paneUtils";
import { PaneComponentEnum } from "../../../../../../subApps/sheet/components/Sidebar/types";
@@ -26,6 +30,7 @@ import MobileDivider from "../../../components/MobileDivider";
import SavingThrowsDetails from "../../../components/SavingThrowsDetails";
import Senses from "../../../components/Senses";
import SubsectionMobile from "../../../components/SubsectionMobile";
import styles from "./styles.module.css";
export default function MainMobile() {
const abilities = useAbilities();
@@ -35,18 +40,18 @@ export default function MainMobile() {
const preferences = useSelector(rulesEngineSelectors.getCharacterPreferences);
const savingThrowDiceAdjustments = useSelector(
rulesEngineSelectors.getSavingThrowDiceAdjustments
); // TODO: GFS move to mangers
);
const situationalBonusSavingThrowsLookup = useSelector(
rulesEngineSelectors.getSituationalBonusSavingThrowsLookup
); // TODO: GFS move to mangers
);
const passivePerception = useSelector(
rulesEngineSelectors.getPassivePerception
); // TODO: GFS move to mangers
);
const passiveInvestigation = useSelector(
rulesEngineSelectors.getPassiveInvestigation
); // TODO: GFS move to mangers
const passiveInsight = useSelector(rulesEngineSelectors.getPassiveInsight); // TODO: GFS move to mangers
const senses = useSelector(rulesEngineSelectors.getSenseInfo); // TODO: GFS move to mangers
);
const passiveInsight = useSelector(rulesEngineSelectors.getPassiveInsight);
const senses = useSelector(rulesEngineSelectors.getSenseInfo);
const ruleData = useSelector(rulesEngineSelectors.getRuleData);
const campaign = useSelector(rulesEngineSelectors.getCampaign);
const theme = useSelector(rulesEngineSelectors.getCharacterTheme);
@@ -56,6 +61,9 @@ export default function MainMobile() {
characterRollContextSelectors.getCharacterRollContext
);
const gameLog = useSelector(appEnvSelectors.getGameLog);
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const handleSensesManageShow = (): void => {
paneHistoryStart(PaneComponentEnum.SENSE_MANAGE);
@@ -90,6 +98,20 @@ export default function MainMobile() {
paneHistoryStart(PaneComponentEnum.CAMPAIGN);
};
const handleShortResetClick = (evt: React.MouseEvent): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(PaneComponentEnum.SHORT_REST);
};
const handleLongResetClick = (evt: React.MouseEvent): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(PaneComponentEnum.LONG_REST);
};
return (
<SubsectionMobile name="Main">
<div
@@ -97,7 +119,31 @@ export default function MainMobile() {
theme.isDarkMode ? "ct-main-mobile--dark-mode" : ""
}`}
>
{campaign && (
{isVttView && (
<div className={styles.restButtonsGroup}>
<Button
variant="outline"
onClick={handleShortResetClick}
themed
size="x-small"
className={styles.restButton}
>
<ThemedShortRestSvg theme={theme} />
<span>Short Rest</span>
</Button>
<Button
variant="outline"
onClick={handleLongResetClick}
themed
size="x-small"
className={styles.restButton}
>
<ThemedLongRestSvg theme={theme} />
<span>Long Rest</span>
</Button>
</div>
)}
{!isVttView && campaign && !isReadonly && (
<CampaignSummary
campaign={campaign}
onCampaignShow={handleCampaignShow}
@@ -111,7 +157,7 @@ export default function MainMobile() {
/>
)}
<section className="ct-main-mobile__abilities">
<h2 style={visuallyHidden}>Ability Scores</h2>
<h2 className={a11yStyles.screenreaderOnly}>Ability Scores</h2>
{abilities.map((ability) => (
<div className="ct-main-mobile__ability" key={ability.getStatKey()}>
<AbilitySummary
@@ -135,7 +181,7 @@ export default function MainMobile() {
theme.isDarkMode ? "ct-main-mobile__saving-throws--dark-mode" : ""
}`}
>
<h2 style={visuallyHidden}>Saving Throws</h2>
<h2 className={a11yStyles.screenreaderOnly}>Saving Throws</h2>
<SavingThrowsSummary
abilities={abilities}
situationalBonusSavingThrowsLookup={
@@ -156,17 +202,18 @@ export default function MainMobile() {
</section>
<MobileDivider isEnd={true} theme={theme} />
<MobileDivider
label="Senses"
label="Passive Senses"
onClick={handleSensesManageShow}
theme={theme}
/>
<h2 style={visuallyHidden}>Senses</h2>
<h2 className={a11yStyles.screenreaderOnly}>Passive Senses</h2>
<Senses
senses={senses}
theme={theme}
passiveInsight={Number(passiveInsight)}
passiveInvestigation={Number(passiveInvestigation)}
passivePerception={Number(passivePerception)}
rowStyle="minimal"
onClick={handleSensesManageShow}
/>
<MobileDivider isEnd={true} theme={theme} />
@@ -1,18 +1,22 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
CharacterTheme,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import a11yStyles from "~/styles/accessibility.module.css";
import MobileDivider from "../../../components/MobileDivider";
import SubsectionMobile from "../../../components/SubsectionMobile";
import { SheetAppState } from "../../../typings";
import Notes from "../../Notes";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
theme: CharacterTheme;
}
class NotesMobile extends React.PureComponent<Props> {
@@ -23,7 +27,7 @@ class NotesMobile extends React.PureComponent<Props> {
<SubsectionMobile className="ct-notes-mobile">
<MobileDivider label="Notes" theme={theme} />
<section>
<h2 style={visuallyHidden}>Notes</h2>
<h2 className={a11yStyles.screenreaderOnly}>Notes</h2>
<Notes />
</section>
<MobileDivider isEnd={true} theme={theme} />
@@ -1,9 +1,9 @@
import { visuallyHidden } from "@mui/utils";
import { FC } from "react";
import { useSelector } from "react-redux";
import { useSidebar } from "~/contexts/Sidebar";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { appEnvSelectors } from "../../../../Shared/selectors";
@@ -35,7 +35,9 @@ export const ProficiencyGroupsMobile: FC<Props> = () => {
theme={characterTheme}
/>
<section className="ct-proficiency-groups-mobile">
<h2 style={visuallyHidden}>Proficiencies and Training</h2>
<h2 className={a11yStyles.screenreaderOnly}>
Proficiencies and Training
</h2>
<ProficiencyGroups
proficiencyGroups={proficiencyGroups}
onClick={handleManageShow}
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect } from "react-redux";
@@ -10,10 +9,11 @@ import {
CharacterTheme,
RuleData,
} from "@dndbeyond/character-rules-engine/es";
import { IRollContext } from "@dndbeyond/dice";
import { RollContext } from "@dndbeyond/pocket-dimension-dice/types";
import { useSidebar } from "~/contexts/Sidebar";
import { PaneInfo } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import {
@@ -33,7 +33,7 @@ interface Props {
theme: CharacterTheme;
diceEnabled: boolean;
ruleData: RuleData;
characterRollContext: IRollContext;
characterRollContext: RollContext;
paneHistoryStart: PaneInfo["paneHistoryStart"];
}
class SkillsBox extends React.PureComponent<Props> {
@@ -88,7 +88,7 @@ class SkillsBox extends React.PureComponent<Props> {
onClick={this.handleManageShow}
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 { connect } from "react-redux";
@@ -8,6 +7,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { appEnvSelectors } from "../../../../Shared/selectors";
@@ -42,7 +42,7 @@ class SpellsMobile extends React.PureComponent<Props> {
theme={theme}
/>
<section className="ct-spells-mobile">
<h2 style={visuallyHidden}>Spells</h2>
<h2 className={a11yStyles.screenreaderOnly}>Spells</h2>
<Spells showNotes={false} />
</section>
<MobileDivider isEnd={true} theme={theme} />
@@ -1,19 +1,23 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
CharacterTheme,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import a11yStyles from "~/styles/accessibility.module.css";
import { appEnvSelectors } from "../../../../Shared/selectors";
import SubsectionTablet from "../../../components/SubsectionTablet";
import TabletBox from "../../../components/TabletBox";
import { SheetAppState } from "../../../typings";
import Actions from "../../Actions";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
isReadonly: boolean;
theme: CharacterTheme;
}
@@ -25,7 +29,7 @@ class ActionsTablet extends React.PureComponent<Props> {
<SubsectionTablet>
<TabletBox theme={theme} header="Actions" className="ct-actions-tablet">
<section>
<h2 style={visuallyHidden}>Actions</h2>
<h2 className={a11yStyles.screenreaderOnly}>Actions</h2>
<Actions />
</section>
</TabletBox>
@@ -1,7 +1,7 @@
import { visuallyHidden } from "@mui/utils";
import { FC } from "react";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import a11yStyles from "~/styles/accessibility.module.css";
import SubsectionTablet from "../../../components/SubsectionTablet";
import TabletBox from "../../../components/TabletBox";
@@ -18,7 +18,7 @@ export const DescriptionTablet: FC = () => {
theme={characterTheme}
>
<section>
<h2 style={visuallyHidden}>Background</h2>
<h2 className={a11yStyles.screenreaderOnly}>Background</h2>
<Description theme={characterTheme} />
</section>
</TabletBox>
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect } from "react-redux";
@@ -8,6 +7,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { appEnvSelectors } from "../../../../Shared/selectors";
@@ -43,7 +43,7 @@ class EquipmentTablet extends React.PureComponent<Props> {
theme={theme}
>
<section>
<h2 style={visuallyHidden}>Inventory</h2>
<h2 className={a11yStyles.screenreaderOnly}>Inventory</h2>
<Equipment />
</section>
</TabletBox>
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect } from "react-redux";
@@ -8,6 +7,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { appEnvSelectors } from "../../../../Shared/selectors";
@@ -43,7 +43,7 @@ class ExtrasTablet extends React.PureComponent<Props> {
theme={theme}
>
<section>
<h2 style={visuallyHidden}>Extras</h2>
<h2 className={a11yStyles.screenreaderOnly}>Extras</h2>
<Extras />
</section>
</TabletBox>
@@ -1,18 +1,22 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
CharacterTheme,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import a11yStyles from "~/styles/accessibility.module.css";
import SubsectionTablet from "../../../components/SubsectionTablet";
import TabletBox from "../../../components/TabletBox";
import { SheetAppState } from "../../../typings";
import Features from "../../Features";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
theme: CharacterTheme;
}
class FeaturesTablet extends React.PureComponent<Props> {
@@ -27,7 +31,7 @@ class FeaturesTablet extends React.PureComponent<Props> {
theme={theme}
>
<section>
<h2 style={visuallyHidden}>Features and Traits</h2>
<h2 className={a11yStyles.screenreaderOnly}>Features and Traits</h2>
<Features />
</section>
</TabletBox>
@@ -1,5 +1,5 @@
import { visuallyHidden } from "@mui/utils";
import { useSelector } from "react-redux";
import { useSearchParams } from "react-router-dom";
import {
AbilitySummary,
@@ -15,6 +15,7 @@ import {
import { useSidebar } from "~/contexts/Sidebar";
import { useAbilities } from "~/hooks/useAbilities";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "../../../../../../subApps/sheet/components/Sidebar/types";
import {
@@ -67,6 +68,9 @@ export default function MainTablet() {
characterRollContextSelectors.getCharacterRollContext
);
const gameLog = useSelector(appEnvSelectors.getGameLog);
const [searchParams] = useSearchParams();
const isVttView = searchParams.get("view") === "vtt";
const handleSensesManageShow = (): void => {
paneHistoryStart(PaneComponentEnum.SENSE_MANAGE);
};
@@ -120,7 +124,7 @@ export default function MainTablet() {
return (
<SubsectionTablet name="Main">
<div className="ct-main-tablet">
{campaign && (
{!isVttView && campaign && !isReadonly && (
<CampaignSummary
campaign={campaign}
onCampaignShow={handleCampaignShow}
@@ -134,7 +138,7 @@ export default function MainTablet() {
/>
)}
<section className="ct-main-tablet__abilities">
<h2 style={visuallyHidden}>Abilities</h2>
<h2 className={a11yStyles.screenreaderOnly}>Abilities</h2>
{abilities.map((ability) => (
<div className="ct-main-tablet__ability" key={ability.getStatKey()}>
<AbilitySummary
@@ -1,18 +1,22 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect, DispatchProp } from "react-redux";
import { AnyAction } from "redux";
import {
CharacterTheme,
rulesEngineSelectors,
} from "@dndbeyond/character-rules-engine/es";
import a11yStyles from "~/styles/accessibility.module.css";
import SubsectionTablet from "../../../components/SubsectionTablet";
import TabletBox from "../../../components/TabletBox";
import { SheetAppState } from "../../../typings";
import Notes from "../../Notes";
interface Props extends DispatchProp {
// TODO: We are typing with AnyAction here 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.
interface Props extends DispatchProp<AnyAction> {
theme: CharacterTheme;
}
class NotesTablet extends React.PureComponent<Props> {
@@ -23,7 +27,7 @@ class NotesTablet extends React.PureComponent<Props> {
<SubsectionTablet>
<TabletBox header="Notes" className="ct-notes-tablet" theme={theme}>
<section>
<h2 style={visuallyHidden}>Notes</h2>
<h2 className={a11yStyles.screenreaderOnly}>Notes</h2>
<Notes />
</section>
</TabletBox>
@@ -1,4 +1,3 @@
import { visuallyHidden } from "@mui/utils";
import React from "react";
import { connect } from "react-redux";
@@ -8,6 +7,7 @@ import {
} from "@dndbeyond/character-rules-engine/es";
import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar";
import a11yStyles from "~/styles/accessibility.module.css";
import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types";
import { appEnvSelectors } from "../../../../Shared/selectors";
@@ -43,7 +43,7 @@ class SpellsTablet extends React.PureComponent<Props> {
theme={theme}
>
<section>
<h2 style={visuallyHidden}>Spells</h2>
<h2 className={a11yStyles.screenreaderOnly}>Spells</h2>
<Spells />
</section>
</TabletBox>