feat(chess): add rule-toggle UI with compatibility warnings (P3.11)
This commit is contained in:
parent
d367f51171
commit
116cbb42b5
2 changed files with 106 additions and 9 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { Routes, Route } from 'react-router-dom'
|
||||
import { GameView } from '../ui/GameView'
|
||||
import { RulesView } from '../ui/RulesView'
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
|
|
@ -7,7 +8,7 @@ export function App() {
|
|||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/game" element={<GameView />} />
|
||||
<Route path="/rules" element={<Rules />} />
|
||||
<Route path="/rules" element={<RulesView />} />
|
||||
<Route path="/save" element={<Save />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
|
@ -22,14 +23,6 @@ function Home() {
|
|||
)
|
||||
}
|
||||
|
||||
function Rules() {
|
||||
return (
|
||||
<main data-testid="page-rules">
|
||||
<h1>Rules</h1>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function Save() {
|
||||
return (
|
||||
<main data-testid="page-save">
|
||||
|
|
|
|||
104
packages/chess/src/ui/RulesView.tsx
Normal file
104
packages/chess/src/ui/RulesView.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { useState, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { PRESET_REGISTRY } from '../presets/index.js';
|
||||
|
||||
export function RulesView({ isGameActive }: { isGameActive?: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
|
||||
const presets = PRESET_REGISTRY.getAll();
|
||||
|
||||
const hasIncompatibilities = useMemo(() => {
|
||||
for (const a of selected) {
|
||||
const aDef = presets.find((p) => p.id === a);
|
||||
if (!aDef) continue;
|
||||
for (const b of selected) {
|
||||
if (a === b) continue;
|
||||
if (aDef.incompatibleWith.includes(b)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, [selected, presets]);
|
||||
|
||||
const togglePreset = (id: string) => {
|
||||
if (isGameActive) return;
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
setSelected(new Set());
|
||||
navigate('/game');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-3xl mx-auto space-y-6" data-testid="page-rules">
|
||||
<h1 className="text-3xl font-bold">Preset Rules</h1>
|
||||
|
||||
{isGameActive && (
|
||||
<div className="bg-gray-100 text-gray-700 p-4 rounded-md">
|
||||
Rules can only be changed between games
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasIncompatibilities && (
|
||||
<div data-testid="compat-warning" className="bg-amber-100 text-amber-800 p-4 rounded-md font-semibold">
|
||||
Warning: Some selected rules are mutually incompatible.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{presets.map((preset) => (
|
||||
<div
|
||||
key={preset.id}
|
||||
data-preset={preset.id}
|
||||
className={`border rounded-lg p-4 flex items-center justify-between transition-colors ${
|
||||
isGameActive ? 'opacity-50 grayscale' : 'hover:border-blue-400'
|
||||
}`}
|
||||
>
|
||||
<div className="pr-4">
|
||||
<h3 className="text-lg font-semibold">{preset.name}</h3>
|
||||
<p className="text-sm text-gray-600 mt-1">{preset.description}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-role="toggle"
|
||||
disabled={isGameActive}
|
||||
onClick={() => togglePreset(preset.id)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 ${
|
||||
selected.has(preset.id) ? 'bg-blue-600' : 'bg-gray-200'
|
||||
} ${isGameActive ? 'cursor-not-allowed' : ''}`}
|
||||
role="switch"
|
||||
aria-checked={selected.has(preset.id)}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||
selected.has(preset.id) ? 'translate-x-5' : 'translate-x-0'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-6">
|
||||
<button
|
||||
type="button"
|
||||
data-action="start-new-game"
|
||||
onClick={handleApply}
|
||||
className="w-full bg-blue-600 text-white rounded-lg py-3 px-4 font-semibold hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Apply and start new game
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue