From e23e69e0d0586ad850ca31e12fd8af446fa4ae04 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 18 Apr 2026 22:35:06 -0600 Subject: [PATCH] feat(ui): modifier profile editor shell + rules drawer entry Adds ModifierProfileEditor modal shell with 3 placeholder panels (T21 catalog / T22 board preview / T23 profile list). Esc closes the modal via a window keydown listener active only while isOpen. Wires a 'Modifier Profiles' button into the RulesDrawer footer that opens the editor. Adds e2e/modifier-profiles.spec.ts with 2 tests: open-from-drawer and esc-to-close. --- packages/chess/e2e/modifier-profiles.spec.ts | 55 +++++++++++++ .../chess/src/ui/ModifierProfileEditor.tsx | 80 +++++++++++++++++++ packages/chess/src/ui/RulesDrawer.tsx | 25 +++++- 3 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 packages/chess/e2e/modifier-profiles.spec.ts create mode 100644 packages/chess/src/ui/ModifierProfileEditor.tsx diff --git a/packages/chess/e2e/modifier-profiles.spec.ts b/packages/chess/e2e/modifier-profiles.spec.ts new file mode 100644 index 0000000..a9c5240 --- /dev/null +++ b/packages/chess/e2e/modifier-profiles.spec.ts @@ -0,0 +1,55 @@ +/** + * E2E — Modifier Profile Editor shell (T18). + * + * Verifies: + * 1. The editor modal opens from the Rules drawer. + * 2. Pressing Escape closes the editor. + * + * Runs against the local dev server (no WS server needed — solo play only). + */ +import { test, expect } from '@playwright/test'; + +test.describe('Modifier Profiles', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + + // Clear any stale autosave so Play Solo starts a fresh game. + await page.evaluate(() => { + for (let i = localStorage.length - 1; i >= 0; i--) { + const key = localStorage.key(i); + if (key !== null && key.startsWith('paratype-chess:v2:autosave:')) { + localStorage.removeItem(key); + } + } + localStorage.removeItem('paratype-chess:v1:autosave'); + }); + + // Navigate into the game view. + await page.locator('[data-action="play-solo"]').click(); + await page.waitForURL('**/game'); + + // Open the rules drawer so the modifier editor button is accessible. + await page.locator('[data-action="open-rules-drawer"]').click(); + await expect(page.getByTestId('rules-drawer')).toBeVisible(); + }); + + test('editor opens from rules drawer', async ({ page }) => { + await page.click('[data-testid="open-modifier-editor"]'); + await expect( + page.locator('[data-testid="modifier-editor-modal"]'), + ).toBeVisible({ timeout: 1000 }); + }); + + test('esc closes modifier editor', async ({ page }) => { + await page.click('[data-testid="open-modifier-editor"]'); + await expect( + page.locator('[data-testid="modifier-editor-modal"]'), + ).toBeVisible(); + + await page.keyboard.press('Escape'); + + await expect( + page.locator('[data-testid="modifier-editor-modal"]'), + ).not.toBeVisible({ timeout: 500 }); + }); +}); diff --git a/packages/chess/src/ui/ModifierProfileEditor.tsx b/packages/chess/src/ui/ModifierProfileEditor.tsx new file mode 100644 index 0000000..c431811 --- /dev/null +++ b/packages/chess/src/ui/ModifierProfileEditor.tsx @@ -0,0 +1,80 @@ +/** + * Modifier Profile Editor — modal shell. + * + * Shell only: three placeholder panels filled by later tasks: + * T21 — modifier catalog (left panel) + * T22 — board preview (center panel) + * T23 — profile list (right panel) + * + * Follows the same overlay/close pattern as LayoutEditor.tsx. + */ +import { useEffect } from 'react'; + +interface Props { + isOpen: boolean; + onClose: () => void; +} + +export function ModifierProfileEditor({ isOpen, onClose }: Props) { + // Register Esc listener only while the modal is visible. + useEffect(() => { + if (!isOpen) return; + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') onClose(); + } + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

+ Modifier Profiles +

+ +
+ + {/* Body — three placeholder panels, filled by T21 / T22 / T23 */} +
+ + +
+

+ Board preview (T22) +

+
+ + +
+
+
+ ); +} diff --git a/packages/chess/src/ui/RulesDrawer.tsx b/packages/chess/src/ui/RulesDrawer.tsx index 2a7074b..0d4e9b6 100644 --- a/packages/chess/src/ui/RulesDrawer.tsx +++ b/packages/chess/src/ui/RulesDrawer.tsx @@ -5,6 +5,7 @@ import type { PresetActivation } from '../net/types.js'; import { AnimatePresence, motion } from 'motion/react'; import { Settings2, X } from 'lucide-react'; import { toast } from 'sonner'; +import { ModifierProfileEditor } from './ModifierProfileEditor.js'; /** * A collapsible side-drawer for toggling preset rules mid-game. @@ -65,6 +66,7 @@ export function RulesDrawer({ onRulesChanged, }: RulesDrawerProps) { const [open, setOpen] = useState(false); + const [modifierEditorOpen, setModifierEditorOpen] = useState(false); const presets = useMemo(() => PRESET_REGISTRY.getAll(), []); const activeById = useMemo(() => { const map = new Map(); @@ -484,15 +486,30 @@ export function RulesDrawer({ })} -