Add rules edition setting for condition tooltips (5e/5.5e)
Introduce a settings modal (opened from the kebab menu) with a rules edition selector for condition tooltip descriptions and a theme picker replacing the inline cycle button. About half the conditions have meaningful mechanical differences between editions. - Add description5e field to ConditionDefinition with 5e (2014) text - Add RulesEditionProvider context with localStorage persistence - Create SettingsModal with Conditions and Theme sections - Wire condition tooltips to edition-aware descriptions - Fix 6 inaccurate 5.5e condition descriptions - Update spec 003 with stories CC-3, CC-8 and FR-095–FR-102 Closes #12 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ActionBar } from "./components/action-bar.js";
|
||||
import { BulkImportToasts } from "./components/bulk-import-toasts.js";
|
||||
import { CombatantRow } from "./components/combatant-row.js";
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PlayerCharacterSection,
|
||||
type PlayerCharacterSectionHandle,
|
||||
} from "./components/player-character-section.js";
|
||||
import { SettingsModal } from "./components/settings-modal.js";
|
||||
import { StatBlockPanel } from "./components/stat-block-panel.js";
|
||||
import { Toast } from "./components/toast.js";
|
||||
import { TurnNavigation } from "./components/turn-navigation.js";
|
||||
@@ -23,6 +24,7 @@ export function App() {
|
||||
|
||||
useAutoStatBlock();
|
||||
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const playerCharacterRef = useRef<PlayerCharacterSectionHandle>(null);
|
||||
const actionBarInputRef = useRef<HTMLInputElement>(null);
|
||||
const activeRowRef = useRef<HTMLDivElement>(null);
|
||||
@@ -62,6 +64,7 @@ export function App() {
|
||||
onManagePlayers={() =>
|
||||
playerCharacterRef.current?.openManagement()
|
||||
}
|
||||
onOpenSettings={() => setSettingsOpen(true)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
@@ -90,6 +93,7 @@ export function App() {
|
||||
onManagePlayers={() =>
|
||||
playerCharacterRef.current?.openManagement()
|
||||
}
|
||||
onOpenSettings={() => setSettingsOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -120,6 +124,10 @@ export function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<SettingsModal
|
||||
open={settingsOpen}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
<PlayerCharacterSection ref={playerCharacterRef} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
EncounterProvider,
|
||||
InitiativeRollsProvider,
|
||||
PlayerCharactersProvider,
|
||||
RulesEditionProvider,
|
||||
SidePanelProvider,
|
||||
ThemeProvider,
|
||||
} from "../contexts/index.js";
|
||||
@@ -12,17 +13,19 @@ import {
|
||||
export function AllProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<EncounterProvider>
|
||||
<BestiaryProvider>
|
||||
<PlayerCharactersProvider>
|
||||
<BulkImportProvider>
|
||||
<SidePanelProvider>
|
||||
<InitiativeRollsProvider>{children}</InitiativeRollsProvider>
|
||||
</SidePanelProvider>
|
||||
</BulkImportProvider>
|
||||
</PlayerCharactersProvider>
|
||||
</BestiaryProvider>
|
||||
</EncounterProvider>
|
||||
<RulesEditionProvider>
|
||||
<EncounterProvider>
|
||||
<BestiaryProvider>
|
||||
<PlayerCharactersProvider>
|
||||
<BulkImportProvider>
|
||||
<SidePanelProvider>
|
||||
<InitiativeRollsProvider>{children}</InitiativeRollsProvider>
|
||||
</SidePanelProvider>
|
||||
</BulkImportProvider>
|
||||
</PlayerCharactersProvider>
|
||||
</BestiaryProvider>
|
||||
</EncounterProvider>
|
||||
</RulesEditionProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { createRef, type RefObject } from "react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { RulesEditionProvider } from "../../contexts/index.js";
|
||||
import { ConditionPicker } from "../condition-picker";
|
||||
|
||||
afterEach(cleanup);
|
||||
@@ -24,12 +25,14 @@ function renderPicker(
|
||||
document.body.appendChild(anchor);
|
||||
(anchorRef as { current: HTMLElement }).current = anchor;
|
||||
const result = render(
|
||||
<ConditionPicker
|
||||
anchorRef={anchorRef}
|
||||
activeConditions={overrides.activeConditions ?? []}
|
||||
onToggle={onToggle}
|
||||
onClose={onClose}
|
||||
/>,
|
||||
<RulesEditionProvider>
|
||||
<ConditionPicker
|
||||
anchorRef={anchorRef}
|
||||
activeConditions={overrides.activeConditions ?? []}
|
||||
onToggle={onToggle}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</RulesEditionProvider>,
|
||||
);
|
||||
return { ...result, onToggle, onClose };
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ import {
|
||||
Import,
|
||||
Library,
|
||||
Minus,
|
||||
Monitor,
|
||||
Moon,
|
||||
Plus,
|
||||
Sun,
|
||||
Settings,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import React, {
|
||||
@@ -25,7 +23,6 @@ import { useEncounterContext } from "../contexts/encounter-context.js";
|
||||
import { useInitiativeRollsContext } from "../contexts/initiative-rolls-context.js";
|
||||
import { usePlayerCharactersContext } from "../contexts/player-characters-context.js";
|
||||
import { useSidePanelContext } from "../contexts/side-panel-context.js";
|
||||
import { useThemeContext } from "../contexts/theme-context.js";
|
||||
import { useLongPress } from "../hooks/use-long-press.js";
|
||||
import { cn } from "../lib/utils.js";
|
||||
import { D20Icon } from "./d20-icon.js";
|
||||
@@ -44,6 +41,7 @@ interface ActionBarProps {
|
||||
inputRef?: RefObject<HTMLInputElement | null>;
|
||||
autoFocus?: boolean;
|
||||
onManagePlayers?: () => void;
|
||||
onOpenSettings?: () => void;
|
||||
}
|
||||
|
||||
function creatureKey(r: SearchResult): string {
|
||||
@@ -216,26 +214,13 @@ function AddModeSuggestions({
|
||||
);
|
||||
}
|
||||
|
||||
const THEME_ICONS = {
|
||||
system: Monitor,
|
||||
light: Sun,
|
||||
dark: Moon,
|
||||
} as const;
|
||||
|
||||
const THEME_LABELS = {
|
||||
system: "Theme: System",
|
||||
light: "Theme: Light",
|
||||
dark: "Theme: Dark",
|
||||
} as const;
|
||||
|
||||
function buildOverflowItems(opts: {
|
||||
onManagePlayers?: () => void;
|
||||
onOpenSourceManager?: () => void;
|
||||
bestiaryLoaded: boolean;
|
||||
onBulkImport?: () => void;
|
||||
bulkImportDisabled?: boolean;
|
||||
themePreference?: "system" | "light" | "dark";
|
||||
onCycleTheme?: () => void;
|
||||
onOpenSettings?: () => void;
|
||||
}): OverflowMenuItem[] {
|
||||
const items: OverflowMenuItem[] = [];
|
||||
if (opts.onManagePlayers) {
|
||||
@@ -260,14 +245,11 @@ function buildOverflowItems(opts: {
|
||||
disabled: opts.bulkImportDisabled,
|
||||
});
|
||||
}
|
||||
if (opts.onCycleTheme) {
|
||||
const pref = opts.themePreference ?? "system";
|
||||
const ThemeIcon = THEME_ICONS[pref];
|
||||
if (opts.onOpenSettings) {
|
||||
items.push({
|
||||
icon: <ThemeIcon className="h-4 w-4" />,
|
||||
label: THEME_LABELS[pref],
|
||||
onClick: opts.onCycleTheme,
|
||||
keepOpen: true,
|
||||
icon: <Settings className="h-4 w-4" />,
|
||||
label: "Settings",
|
||||
onClick: opts.onOpenSettings,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
@@ -277,6 +259,7 @@ export function ActionBar({
|
||||
inputRef,
|
||||
autoFocus,
|
||||
onManagePlayers,
|
||||
onOpenSettings,
|
||||
}: Readonly<ActionBarProps>) {
|
||||
const {
|
||||
addCombatant,
|
||||
@@ -290,7 +273,6 @@ export function ActionBar({
|
||||
const { characters: playerCharacters } = usePlayerCharactersContext();
|
||||
const { showBulkImport, showSourceManager, showCreature, panelView } =
|
||||
useSidePanelContext();
|
||||
const { preference: themePreference, cycleTheme } = useThemeContext();
|
||||
const { handleRollAllInitiative } = useInitiativeRollsContext();
|
||||
const { state: bulkImportState } = useBulkImportContext();
|
||||
|
||||
@@ -532,8 +514,7 @@ export function ActionBar({
|
||||
bestiaryLoaded,
|
||||
onBulkImport: showBulkImport,
|
||||
bulkImportDisabled: bulkImportState.status === "loading",
|
||||
themePreference,
|
||||
onCycleTheme: cycleTheme,
|
||||
onOpenSettings,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { CONDITION_DEFINITIONS, type ConditionId } from "@initiative/domain";
|
||||
import {
|
||||
CONDITION_DEFINITIONS,
|
||||
type ConditionId,
|
||||
getConditionDescription,
|
||||
} from "@initiative/domain";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowDown,
|
||||
@@ -19,6 +23,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
||||
import { cn } from "../lib/utils";
|
||||
import { Tooltip } from "./ui/tooltip.js";
|
||||
|
||||
@@ -104,6 +109,7 @@ export function ConditionPicker({
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [onClose]);
|
||||
|
||||
const { edition } = useRulesEditionContext();
|
||||
const active = new Set(activeConditions ?? []);
|
||||
|
||||
return createPortal(
|
||||
@@ -122,7 +128,11 @@ export function ConditionPicker({
|
||||
const isActive = active.has(def.id);
|
||||
const colorClass = COLOR_CLASSES[def.color] ?? "text-muted-foreground";
|
||||
return (
|
||||
<Tooltip key={def.id} content={def.description} className="block">
|
||||
<Tooltip
|
||||
key={def.id}
|
||||
content={getConditionDescription(def, edition)}
|
||||
className="block"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { CONDITION_DEFINITIONS, type ConditionId } from "@initiative/domain";
|
||||
import {
|
||||
CONDITION_DEFINITIONS,
|
||||
type ConditionId,
|
||||
getConditionDescription,
|
||||
} from "@initiative/domain";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
ArrowDown,
|
||||
@@ -18,6 +22,7 @@ import {
|
||||
Sparkles,
|
||||
ZapOff,
|
||||
} from "lucide-react";
|
||||
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
||||
import { cn } from "../lib/utils.js";
|
||||
import { Tooltip } from "./ui/tooltip.js";
|
||||
|
||||
@@ -63,6 +68,7 @@ export function ConditionTags({
|
||||
onRemove,
|
||||
onOpenPicker,
|
||||
}: Readonly<ConditionTagsProps>) {
|
||||
const { edition } = useRulesEditionContext();
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-0.5">
|
||||
{conditions?.map((condId) => {
|
||||
@@ -72,7 +78,10 @@ export function ConditionTags({
|
||||
if (!Icon) return null;
|
||||
const colorClass = COLOR_CLASSES[def.color] ?? "text-muted-foreground";
|
||||
return (
|
||||
<Tooltip key={condId} content={`${def.label}: ${def.description}`}>
|
||||
<Tooltip
|
||||
key={condId}
|
||||
content={`${def.label}:\n${getConditionDescription(def, edition)}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Remove ${def.label}`}
|
||||
|
||||
129
apps/web/src/components/settings-modal.tsx
Normal file
129
apps/web/src/components/settings-modal.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { RulesEdition } from "@initiative/domain";
|
||||
import { Monitor, Moon, Sun, X } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
||||
import { useThemeContext } from "../contexts/theme-context.js";
|
||||
import { cn } from "../lib/utils.js";
|
||||
import { Button } from "./ui/button.js";
|
||||
|
||||
interface SettingsModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const EDITION_OPTIONS: { value: RulesEdition; label: string }[] = [
|
||||
{ value: "5e", label: "5e (2014)" },
|
||||
{ value: "5.5e", label: "5.5e (2024)" },
|
||||
];
|
||||
|
||||
const THEME_OPTIONS: {
|
||||
value: "system" | "light" | "dark";
|
||||
label: string;
|
||||
icon: typeof Sun;
|
||||
}[] = [
|
||||
{ value: "system", label: "System", icon: Monitor },
|
||||
{ value: "light", label: "Light", icon: Sun },
|
||||
{ value: "dark", label: "Dark", icon: Moon },
|
||||
];
|
||||
|
||||
export function SettingsModal({ open, onClose }: Readonly<SettingsModalProps>) {
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
const { edition, setEdition } = useRulesEditionContext();
|
||||
const { preference, setPreference } = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
if (open && !dialog.open) dialog.showModal();
|
||||
else if (!open && dialog.open) dialog.close();
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
function handleCancel(e: Event) {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
function handleBackdropClick(e: MouseEvent) {
|
||||
if (e.target === dialog) onClose();
|
||||
}
|
||||
dialog.addEventListener("cancel", handleCancel);
|
||||
dialog.addEventListener("mousedown", handleBackdropClick);
|
||||
return () => {
|
||||
dialog.removeEventListener("cancel", handleCancel);
|
||||
dialog.removeEventListener("mousedown", handleBackdropClick);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
className="card-glow m-auto w-full max-w-sm rounded-lg border border-border bg-card p-6 backdrop:bg-black/50"
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="font-semibold text-foreground text-lg">Settings</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<X size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5">
|
||||
<div>
|
||||
<span className="mb-2 block font-medium text-muted-foreground text-sm">
|
||||
Conditions
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
{EDITION_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex-1 rounded-md px-3 py-1.5 text-sm transition-colors",
|
||||
edition === opt.value
|
||||
? "bg-accent text-primary-foreground"
|
||||
: "bg-card text-muted-foreground hover:bg-hover-neutral-bg hover:text-foreground",
|
||||
)}
|
||||
onClick={() => setEdition(opt.value)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="mb-2 block font-medium text-muted-foreground text-sm">
|
||||
Theme
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
{THEME_OPTIONS.map((opt) => {
|
||||
const Icon = opt.icon;
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-center gap-1.5 rounded-md px-3 py-1.5 text-sm transition-colors",
|
||||
preference === opt.value
|
||||
? "bg-accent text-primary-foreground"
|
||||
: "bg-card text-muted-foreground hover:bg-hover-neutral-bg hover:text-foreground",
|
||||
)}
|
||||
onClick={() => setPreference(opt.value)}
|
||||
>
|
||||
<Icon size={14} />
|
||||
{opt.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export function Tooltip({
|
||||
createPortal(
|
||||
<div
|
||||
role="tooltip"
|
||||
className="pointer-events-none fixed z-[60] max-w-64 -translate-x-1/2 -translate-y-full rounded-md border border-border bg-background px-2.5 py-1.5 text-foreground text-xs leading-snug shadow-lg"
|
||||
className="pointer-events-none fixed z-[60] max-w-64 -translate-x-1/2 -translate-y-full whitespace-pre-line rounded-md border border-border bg-background px-2.5 py-1.5 text-foreground text-xs leading-snug shadow-lg"
|
||||
style={{ top: pos.top, left: pos.left }}
|
||||
>
|
||||
{content}
|
||||
|
||||
@@ -3,5 +3,6 @@ export { BulkImportProvider } from "./bulk-import-context.js";
|
||||
export { EncounterProvider } from "./encounter-context.js";
|
||||
export { InitiativeRollsProvider } from "./initiative-rolls-context.js";
|
||||
export { PlayerCharactersProvider } from "./player-characters-context.js";
|
||||
export { RulesEditionProvider } from "./rules-edition-context.js";
|
||||
export { SidePanelProvider } from "./side-panel-context.js";
|
||||
export { ThemeProvider } from "./theme-context.js";
|
||||
|
||||
24
apps/web/src/contexts/rules-edition-context.tsx
Normal file
24
apps/web/src/contexts/rules-edition-context.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { createContext, type ReactNode, useContext } from "react";
|
||||
import { useRulesEdition } from "../hooks/use-rules-edition.js";
|
||||
|
||||
type RulesEditionContextValue = ReturnType<typeof useRulesEdition>;
|
||||
|
||||
const RulesEditionContext = createContext<RulesEditionContextValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function RulesEditionProvider({ children }: { children: ReactNode }) {
|
||||
const value = useRulesEdition();
|
||||
return (
|
||||
<RulesEditionContext.Provider value={value}>
|
||||
{children}
|
||||
</RulesEditionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useRulesEditionContext(): RulesEditionContextValue {
|
||||
const ctx = useContext(RulesEditionContext);
|
||||
if (!ctx)
|
||||
throw new Error("useRulesEditionContext requires RulesEditionProvider");
|
||||
return ctx;
|
||||
}
|
||||
52
apps/web/src/hooks/use-rules-edition.ts
Normal file
52
apps/web/src/hooks/use-rules-edition.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { RulesEdition } from "@initiative/domain";
|
||||
import { useCallback, useSyncExternalStore } from "react";
|
||||
|
||||
const STORAGE_KEY = "initiative:rules-edition";
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
let currentEdition: RulesEdition = loadEdition();
|
||||
|
||||
function loadEdition(): RulesEdition {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw === "5e" || raw === "5.5e") return raw;
|
||||
} catch {
|
||||
// storage unavailable
|
||||
}
|
||||
return "5.5e";
|
||||
}
|
||||
|
||||
function saveEdition(edition: RulesEdition): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, edition);
|
||||
} catch {
|
||||
// quota exceeded or storage unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function notifyAll(): void {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function subscribe(callback: () => void): () => void {
|
||||
listeners.add(callback);
|
||||
return () => listeners.delete(callback);
|
||||
}
|
||||
|
||||
function getSnapshot(): RulesEdition {
|
||||
return currentEdition;
|
||||
}
|
||||
|
||||
export function useRulesEdition() {
|
||||
const edition = useSyncExternalStore(subscribe, getSnapshot);
|
||||
|
||||
const setEdition = useCallback((next: RulesEdition) => {
|
||||
currentEdition = next;
|
||||
saveEdition(next);
|
||||
notifyAll();
|
||||
}, []);
|
||||
|
||||
return { edition, setEdition } as const;
|
||||
}
|
||||
@@ -71,8 +71,6 @@ function getSnapshot(): ThemePreference {
|
||||
return currentPreference;
|
||||
}
|
||||
|
||||
const CYCLE: ThemePreference[] = ["system", "light", "dark"];
|
||||
|
||||
export function useTheme() {
|
||||
const preference = useSyncExternalStore(subscribe, getSnapshot);
|
||||
const resolved = resolve(preference);
|
||||
@@ -88,11 +86,5 @@ export function useTheme() {
|
||||
notifyAll();
|
||||
}, []);
|
||||
|
||||
const cycleTheme = useCallback(() => {
|
||||
const idx = CYCLE.indexOf(currentPreference);
|
||||
const next = CYCLE[(idx + 1) % CYCLE.length];
|
||||
setPreference(next);
|
||||
}, [setPreference]);
|
||||
|
||||
return { preference, resolved, setPreference, cycleTheme } as const;
|
||||
return { preference, resolved, setPreference } as const;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
EncounterProvider,
|
||||
InitiativeRollsProvider,
|
||||
PlayerCharactersProvider,
|
||||
RulesEditionProvider,
|
||||
SidePanelProvider,
|
||||
ThemeProvider,
|
||||
} from "./contexts/index.js";
|
||||
@@ -17,19 +18,21 @@ if (root) {
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider>
|
||||
<EncounterProvider>
|
||||
<BestiaryProvider>
|
||||
<PlayerCharactersProvider>
|
||||
<BulkImportProvider>
|
||||
<SidePanelProvider>
|
||||
<InitiativeRollsProvider>
|
||||
<App />
|
||||
</InitiativeRollsProvider>
|
||||
</SidePanelProvider>
|
||||
</BulkImportProvider>
|
||||
</PlayerCharactersProvider>
|
||||
</BestiaryProvider>
|
||||
</EncounterProvider>
|
||||
<RulesEditionProvider>
|
||||
<EncounterProvider>
|
||||
<BestiaryProvider>
|
||||
<PlayerCharactersProvider>
|
||||
<BulkImportProvider>
|
||||
<SidePanelProvider>
|
||||
<InitiativeRollsProvider>
|
||||
<App />
|
||||
</InitiativeRollsProvider>
|
||||
</SidePanelProvider>
|
||||
</BulkImportProvider>
|
||||
</PlayerCharactersProvider>
|
||||
</BestiaryProvider>
|
||||
</EncounterProvider>
|
||||
</RulesEditionProvider>
|
||||
</ThemeProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user