From 9b586b83b52068b4c73b1d67aafec59c51f98edf Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sun, 19 Apr 2026 20:20:03 -0600 Subject: [PATCH] feat(ui): custom modifiers in modifier profile panels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T3 Wave 4 (T26). PerTypePanel and PerInstancePanel kind dropdowns now include the user's custom modifier library alongside built-ins. - Kind dropdown uses separation: 'Built-in' (MODIFIER_REGISTRY) + 'Custom (from library)' (loadCustomModifierLibrary). - Selecting a custom kind hides the per-instance value input and shows a small summary card ('Custom modifier — N primitives. Edit it in the Custom Modifier editor.') — the descriptor's primitive list IS the payload; per-instance value is null. - Custom kinds skip Zod-schema-based validation (they have no per-instance value to validate); isValid is true once a custom kind is selected. - handleSave / handleAdd branches on isCustomKind: built-in path preserves T1/T2 behaviour exactly; custom path emits {kind: id, pieceType, color, value: null}. - PerInstancePanel mirrors the same pattern in AddModifierForm. E2E tests for the new UI defer to T29; existing 1378 unit tests pass. --- packages/chess/src/ui/PerInstancePanel.tsx | 66 +++++++++++++-- packages/chess/src/ui/PerTypePanel.tsx | 96 +++++++++++++++++----- 2 files changed, 135 insertions(+), 27 deletions(-) diff --git a/packages/chess/src/ui/PerInstancePanel.tsx b/packages/chess/src/ui/PerInstancePanel.tsx index 97951f4..1199fa0 100644 --- a/packages/chess/src/ui/PerInstancePanel.tsx +++ b/packages/chess/src/ui/PerInstancePanel.tsx @@ -11,11 +11,15 @@ * 3. For the selected square → view existing instance modifiers and * add/delete them via the right-hand sidebar form. */ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import type { InstanceModifier, ModifierKindId } from '../modifiers/types'; import type { StartingLayout } from '../layouts/types'; import { squareToAlgebraic } from '../coord'; import { LayoutBoardView } from './LayoutEditor'; +import { + loadCustomModifierLibrary, + type SavedCustomModifier, +} from '../modifiers/custom/library.js'; import type { ModifierClipboard } from './ModifierProfileEditor.js'; export interface PerInstancePanelProps { @@ -264,10 +268,29 @@ function AddModifierForm({ }) { const [kind, setKind] = useState('hp-bonus'); const [rawValue, setRawValue] = useState('0'); + // Custom modifiers from the user's library appear in the kind + // dropdown alongside built-ins (T26). Reload on mount; new entries + // saved via CustomModifierEditor become visible the next time the + // user opens this form. + const [customs, setCustoms] = useState(() => + loadCustomModifierLibrary(), + ); + useEffect(() => { + setCustoms(loadCustomModifierLibrary()); + }, []); + + const customDescriptor = customs.find((c) => c.id === kind); + const isCustomKind = customDescriptor !== undefined; function handleAdd() { - let parsed: unknown; + if (isCustomKind && customDescriptor) { + // Custom kinds carry no per-instance value; the descriptor's + // primitive list is the entire payload. + onAdd({ kind: customDescriptor.id, square, value: null }); + return; + } + let parsed: unknown; if (kind === 'direction-additions') { parsed = rawValue .split(',') @@ -301,14 +324,41 @@ function AddModifierForm({ }} className="w-full px-3 py-2 text-sm border border-neutral-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" > - {MODIFIER_KINDS.map(({ id, label }) => ( - - ))} + + {MODIFIER_KINDS.map(({ id, label }) => ( + + ))} + + {customs.length > 0 && ( + + {customs.map((c) => ( + + ))} + + )} - + {isCustomKind && customDescriptor !== undefined ? ( +
+ Custom modifier — {customDescriptor.descriptor.primitives.length}{' '} + primitive + {customDescriptor.descriptor.primitives.length !== 1 ? 's' : ''}. + Edit it in the Custom Modifier editor. +
+ ) : ( + + )}