PURPOSE
Renders a minimal layer-stack list for the VFX weapon-workbench editor. Each row represents one Layer in the component schema and exposes the controls needed to rearrange and toggle layers without leaving the list: enabled checkbox, kind chip, name input, solo toggle, blend-mode dropdown, move-up / move-down buttons, and delete button. Parameter editing for the selected layer is delegated to a downstream Inspector — this component is intentionally restricted to stack-level concerns.
OWNS
- Local
soloIdstate (useState<string | null>) — which layer, if any, is currently soloed. - The
add,remove,move, andupdatehelpers that derive a newLayer[]from the currentlayersprop and hand it toonChange. - ID-allocation logic for new layers — strips the leading
lfrom existing ids, parses the integer, takes the max, and adds one to produce the nextl<n>id. - The default shape of a newly added layer:
kind: 'fill',shader: 'disc',params: {},blend: 'additive',opacity: 1,enabled: true,name: 'new layer'. - Visual styling —
ROW_STYLE,KIND_COLORperLayerKind, and thebtnMinistyle shared by reorder and delete buttons. - Re-exports of the in-file constants
LAYER_KINDSandBLEND_MODES.
READS FROM
@engine/vfx-workbench/component-schema— type importsLayer,LayerBlendMode,LayerKind.- Props from the parent:
layers: Layer[],selectedLayerId: string | null,onSelect,onChange.
PUSHES TO
onChange(layers: Layer[])— invoked for every structural or per-field edit: reorder, enable toggle, name change, blend-mode change, add, delete.onSelect(layerId: string)— invoked when a row is clicked (excluding clicks on interactive children, which callstopPropagation).
DOES NOT
- Does not edit layer parameters (
params), shaders, opacity, or any field beyondname,enabled, andblend. Those are the Inspector’s job. - Does not persist anything itself — all state lives in the parent via
onChange. - Does not implement true drag-and-drop reorder; reorder is via up / down buttons only.
- Does not allow deletion of the last remaining layer —
removeis a no-op whenlayers.length <= 1, and the delete button is also disabled in that case. - Does not mutate the input
layersarray; every helper slices a copy. - Does not own the authoritative
LayerKind/LayerBlendModeenums — the comment at the bottom of the file defers to@engine/vfx-workbench/component-schema.
Signals
- Selected row is outlined with
borderColor: '#44ffcc'instead of#222. - Disabled or non-soloed-while-solo-active rows render at
opacity: 0.35via theeffectiveEnabledpredicate. - The solo button turns cyan (
#44ffccbackground,#001text) when that layer is the active solo target. - Each
LayerKindhas a distinct kind-chip color: shape#ffaa44, fill#44ffcc, mask#cc88ff, stroke#ff6688, glow#ffee44, post#4488ff. - Empty state — when
layers.length === 0, the component renders an italic placeholderNo layers. Click "+ layer" to add one. - Up button is disabled at the top of the stack (
idx === layers.length - 1); down button is disabled at the bottom (idx === 0); delete button is disabled when only one layer remains. - Header reads
LAYERS (<count>)with an+ layeradd button on the right.
Entry points
- Exported component:
LayerStackEditor({ layers, selectedLayerId, onSelect, onChange }). - Exported props interface:
LayerStackEditorProps. - Re-exported constants:
LAYER_KINDS,BLEND_MODES. data-testidhooks used by tests:layer-stack-editor,add-layer-button, and per-layerlayer-row-<id>,layer-enable-<id>,layer-name-<id>,layer-solo-<id>,layer-blend-<id>,layer-up-<id>,layer-down-<id>,layer-delete-<id>.
Pattern notes
- Render order is reversed from array order:
layers.slice().reverse().map(...)— index 0 is the bottom of the visual stack, so it appears at the bottom of the list. The originalidx(in the unreversedlayersarray) is recovered vialayers.indexOf(layer)and is what the helpers use. - Because of the reversal, the move-up button (
▲) callsmove(idx, +1)and the move-down button (▼) callsmove(idx, -1)— the visual direction is the opposite of the array-index direction. - All interactive children inside a row call
e.stopPropagation()on click so they do not also trigger the row-levelonSelectfrom the parent<div>. - Solo is local UI state, not part of the persisted
Layermodel — leaving and re-entering the editor resets it.effectiveEnabledcombineslayer.enabledwith the local solo gate purely for the visual dim treatment; it does not mutate the layer. update,remove, andmovealways produce a fresh array viaslice/filterand never mutatelayersin place — safe to pass directly to a Zustand or other immutable store.- Inline styles are used throughout; there is no CSS module or class hookup. Font stack is
Space Grotesk, sans-seriffor text andmonospacefor kind chips, mini-buttons, and the blend dropdown. - The component is presentational with one piece of local state — it can be unit-tested by mounting it with a controlled
layersprop and asserting ononChange/onSelectcall shapes.