Support the 2014 DMG encounter difficulty as an alternative to the 5.5e system behind the existing Rules Edition toggle. The 2014 system uses Easy/Medium/Hard/Deadly thresholds, an encounter multiplier based on monster count, and party size adjustment (×0.5–×5 range). - Extract RulesEdition to its own domain module - Refactor DifficultyTier to abstract numeric values (0–3) - Restructure DifficultyResult with thresholds array - Add 2014 XP thresholds table and encounter multiplier logic - Wire edition from context into difficulty hooks - Edition-aware labels in indicator and breakdown panel - Show multiplier, adjusted XP, and party size note for 2014 - Rename settings label from "Conditions" to "Rules Edition" - Update spec 008 with issue #23 requirements Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
90 lines
2.6 KiB
TypeScript
90 lines
2.6 KiB
TypeScript
import type { RulesEdition } from "@initiative/domain";
|
|
import { Monitor, Moon, Sun } from "lucide-react";
|
|
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
|
import { useThemeContext } from "../contexts/theme-context.js";
|
|
import { cn } from "../lib/utils.js";
|
|
import { Dialog, DialogHeader } from "./ui/dialog.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 { edition, setEdition } = useRulesEditionContext();
|
|
const { preference, setPreference } = useThemeContext();
|
|
|
|
return (
|
|
<Dialog open={open} onClose={onClose} className="card-glow w-full max-w-sm">
|
|
<DialogHeader title="Settings" onClose={onClose} />
|
|
|
|
<div className="flex flex-col gap-5">
|
|
<div>
|
|
<span className="mb-2 block font-medium text-muted-foreground text-sm">
|
|
Rules Edition
|
|
</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>
|
|
);
|
|
}
|