feat(modifiers): bundle preset activations on profiles + editor Presets tab

Profiles can now carry a list of PresetActivation entries alongside
their per-type / per-instance modifiers. When such a profile is
picked in the Lobby, its bundled presets are unioned with any
layout suggestedPresets (profile config wins on id-collision).

Changes:

- ModifierProfile.presetActivations added (optional readonly array).
  Zod schema + server wire schema mirror the field; drift guard
  picks up forgotten updates on either side.
- ModifierProfileEditor grows a Presets tab in the right column
  sharing space with the Library tab. Each preset row carries a
  checkbox, scope radio (both/white/black), and a turns counter
  (blank = permanent). Inline diagnostics warn on redundant layout
  overlap and on loose-scope incompatibility; hard errors under
  overlapping scope disable Save.
- Lobby merges profile.presetActivations into its active preset
  set on profile select, on editor close, and on URL deep-link.
- LayoutPicker suggested-preset chips now expose aria-pressed so
  the active state is readable by assistive tech + e2e tests.

Tests: library round-trip preserves presetActivations (unit); two
new e2e scenarios (pre-seeded bundled profile auto-activates; full
editor-author loop persists + reflects in the lobby). 1752 unit +
105 playwright all green.
This commit is contained in:
Joey Yakimowich-Payne 2026-04-21 14:03:52 -06:00
commit 699c288a98
No known key found for this signature in database
9 changed files with 602 additions and 12 deletions

View file

@ -260,6 +260,19 @@ export type InstanceModifierWire = z.infer<typeof InstanceModifierSchema>;
* drift without requiring us to export the chess-side zod schema across
* a zod major-version boundary.
*/
/**
* Wire schema for a bundled preset activation. Shape-mirrored with
* chess-side `PresetActivation` in presets/active-set.ts. We don't
* enum-check `id` against the registry here — the engine tolerates
* unknown ids and the drift guard below only checks key parity with
* the chess-side type.
*/
export const PresetActivationWireSchema = z.object({
id: z.string().min(1),
scope: z.enum(["both", "white", "black"]),
turnsRemaining: z.number().int().positive().nullable(),
});
export const ModifierProfileSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
@ -267,6 +280,7 @@ export const ModifierProfileSchema = z.object({
layoutId: z.string().optional(),
perType: z.array(TypeModifierSchema),
perInstance: z.array(InstanceModifierSchema),
presetActivations: z.array(PresetActivationWireSchema).optional(),
version: z.literal(1),
source: z.enum(["premade", "custom"]),
});