PURPOSE

Modal dialog for creating a new VFX component in the Weapon Workbench. Collects id, display name, category, kind (baked or live_shader), and starting shape template, then writes the new component definition to disk via the dev save endpoint.

OWNS

  • Local form state: id, name, category, kind, selectedShape, saving, error.
  • ID validation (/^[a-zA-Z0-9_]+$/) and name non-empty validation.
  • Construction of the component definition object for either baked or live_shader kinds, including default uniform values, default 4-color palette, default bake settings (frameCount: 12, fps: 30, tileSize: 64, randomized seed), and a default fragment GLSL stub for live shaders.
  • handleCreate async submit flow: serialize def, POST to dev endpoint, invoke onCreated, reset form, surface errors.
  • Backdrop click-to-close behaviour.

READS FROM

  • TEMPLATES and TemplateId from @engine/vfx-workbench/shader-templates/registry — drives the starting-shape picker and supplies the uniform schema used to populate baked-component defaults.
  • Props open, onClose, onCreated supplied by the parent screen.

PUSHES TO

  • POST /__dev/vfx-save with body { kind: 'component', id, content } where content is the JSON-stringified component definition. The dev save endpoint is responsible for writing the file.
  • onCreated(id) callback to notify the parent that a new component is available.
  • onClose() callback on cancel or backdrop click.

DOES NOT

  • Does not persist anything itself — all writes go through the /__dev/vfx-save endpoint.
  • Does not edit existing components (creation only).
  • Does not validate uniqueness of id against existing components.
  • Does not render when open is false (early return null).
  • Does not bake frames or compile shaders; only seeds defaults.
  • Does not interact with Zustand stores directly.

Signals

  • Loopmode default depends on category: muzzle and impact get oneshot; all other categories get loop.
  • Live-shader default blendMode is additive.
  • Default anchors object: { center: { kind: 'pivot' } }.
  • Default baked palette is 4 RGB triples representing the standard cosine-palette parameters (a, b, c, d).
  • Default baked params are pulled from each uniform’s numeric default in the template’s uniformSchema.
  • Live-shader default uniformSchema exposes u_time (float) and u_resolution (vec2); previewSampleInputs seeds u_time: 0 and u_resolution: [64, 64].
  • Form is reset only on successful create; errors leave the form populated.

Entry points

  • Exported named function NewComponentModal({ open, onClose, onCreated }).
  • Rendered by the Weapon Workbench screen when the user requests a new component.
  • Test hooks via data-testid: new-component-modal-backdrop, new-component-modal, new-component-id, new-component-name, new-component-category, new-component-kind, modal-shape-<templateId>, new-component-cancel, new-component-create.

Pattern notes

  • Pure local useState; no external store. Parent owns visibility via the open prop.
  • Inline styles only — matches the workbench’s no-CSS-module convention. Accent color #44ffcc for active selections and the primary action.
  • Discriminated union on kind builds two different shapes of the component def; the shape picker is conditionally rendered only when kind === 'baked'.
  • Errors from the save endpoint are caught and surfaced inline; saving flag disables the create button and swaps its label to Creating….
  • Backdrop close uses e.target === e.currentTarget to avoid swallowing clicks inside the modal panel.
  • Seed is generated with Math.floor(Math.random() * 10000) — non-deterministic per creation.