feat(ui): custom modifier editor in-modal docs, recipes, and attr combobox

Surfaces the contents of docs/user/custom-modifiers.md directly inside
the Custom Modifier Editor so authors can compose descriptors without
cross-referencing the guide:

- Per-primitive docs panel in the Parameter Inspector with a longer
  behaviour explanation + one or more worked examples (collapsible).
- Palette hover tooltips now show the full long description plus the
  first example's headline.
- New 'Templates' header button opens a picker with 5 built-in recipes
  (Boosted Pawn, 3-Charge Shield, Aura King, Vampire, Low-HP Fortress).
- AttrCombobox replaces plain text inputs for attr / targetAttr fields.
  Grouped, free-form autocomplete over 17 curated suggestions with a
  'user-defined' badge for out-of-catalog typed names so ShieldCharges-
  style recipes still work.

Primitives gain optional longDescription + examples fields on their
EffectPrimitive descriptor; 15 registrations annotated. Recipe
descriptors pass the existing validator, and new unit tests enforce
doc coverage going forward.
This commit is contained in:
Joey Yakimowich-Payne 2026-04-21 12:50:14 -06:00
commit 934db775f9
No known key found for this signature in database
25 changed files with 1354 additions and 6 deletions

View file

@ -1322,6 +1322,78 @@ test.describe('T29 — Custom modifier DSL e2e', () => {
await ctxHost.close();
}
});
test('editor shows docs + examples for selected primitive', async ({
page,
}) => {
await freshLobby(page);
await openProfileEditor(page);
await openCustomModifierEditor(page);
// Add a primitive so the inspector renders.
await page.getByTestId('custom-primitive-palette-seed-attribute').click();
await page.getByTestId('custom-primitive-node-0').click();
// Docs panel is present with at least one example.
const docs = page.getByTestId('custom-primitive-docs');
await expect(docs).toBeVisible();
await expect(docs).toContainText('seed-attribute');
await expect(
page.getByTestId('custom-primitive-example-0'),
).toBeVisible();
});
test('AttrCombobox autocompletes known attrs and accepts user-defined names', async ({
page,
}) => {
await freshLobby(page);
await openProfileEditor(page);
await openCustomModifierEditor(page);
// Add a seed-attribute primitive so the attr field renders.
await page.getByTestId('custom-primitive-palette-seed-attribute').click();
await page.getByTestId('custom-primitive-node-0').click();
const combobox = page.getByTestId('primitive-seed-attribute-attr');
await expect(combobox).toBeVisible();
// Focus the input; the dropdown should open with groups visible.
const input = page.getByTestId('primitive-seed-attribute-attr-input');
await input.click();
await expect(
page.getByTestId('primitive-seed-attribute-attr-dropdown'),
).toBeVisible();
// Click the "Hp" suggestion — input value updates and dropdown closes.
await page.getByTestId('attr-combobox-option-Hp').first().click();
await expect(input).toHaveValue('Hp');
// Type a user-defined attr name — badge appears, dropdown filters down.
await input.fill('MyStacks');
await expect(combobox).toContainText('user-defined');
});
test('Templates picker loads a recipe into the editor', async ({ page }) => {
await freshLobby(page);
await openProfileEditor(page);
await openCustomModifierEditor(page);
// Open the templates modal and pick the shield recipe (2 primitives).
await page.getByTestId('custom-templates').click();
await expect(page.getByTestId('custom-templates-modal')).toBeVisible();
await page
.getByTestId('custom-template-recipe-three-charge-shield')
.click();
// Modal closes and the tree is populated with the recipe's primitives.
await expect(page.getByTestId('custom-templates-modal')).toHaveCount(0);
await expect(page.getByTestId('custom-primitive-node-0')).toBeVisible();
await expect(page.getByTestId('custom-primitive-node-1')).toBeVisible();
// Name field reflects the template's descriptor.name.
await expect(
page.locator('input[placeholder="Modifier Name"]'),
).toHaveValue('3-Charge Shield');
});
});
// ─── WS helpers (mirrored from modifier-profiles.spec.ts) ──────────