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
bakedorlive_shaderkinds, including default uniform values, default 4-color palette, default bake settings (frameCount: 12,fps: 30,tileSize: 64, randomizedseed), and a default fragment GLSL stub for live shaders. handleCreateasync submit flow: serialize def, POST to dev endpoint, invokeonCreated, reset form, surface errors.- Backdrop click-to-close behaviour.
READS FROM
TEMPLATESandTemplateIdfrom@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,onCreatedsupplied by the parent screen.
PUSHES TO
POST /__dev/vfx-savewith body{ kind: 'component', id, content }wherecontentis 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-saveendpoint. - Does not edit existing components (creation only).
- Does not validate uniqueness of
idagainst existing components. - Does not render when
openis false (earlyreturn null). - Does not bake frames or compile shaders; only seeds defaults.
- Does not interact with Zustand stores directly.
Signals
- Loopmode default depends on category:
muzzleandimpactgetoneshot; all other categories getloop. - Live-shader default
blendModeisadditive. - 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
defaultin the template’suniformSchema. - Live-shader default
uniformSchemaexposesu_time(float) andu_resolution(vec2);previewSampleInputsseedsu_time: 0andu_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 theopenprop. - Inline styles only — matches the workbench’s no-CSS-module convention. Accent color
#44ffccfor active selections and the primary action. - Discriminated union on
kindbuilds two different shapes of the component def; the shape picker is conditionally rendered only whenkind === 'baked'. - Errors from the save endpoint are caught and surfaced inline;
savingflag disables the create button and swaps its label toCreating…. - Backdrop close uses
e.target === e.currentTargetto avoid swallowing clicks inside the modal panel. - Seed is generated with
Math.floor(Math.random() * 10000)— non-deterministic per creation.