CI/CD
Some checks failed
CI / Native Windows Build And Tests (push) Failing after 21s
CI / React UI Build (push) Has been cancelled

This commit is contained in:
2026-05-03 11:26:10 +10:00
parent ee929374a8
commit c39a1fd53c
12 changed files with 848 additions and 701 deletions

View File

@@ -0,0 +1,154 @@
import { GripVertical, Trash2 } from "lucide-react";
import { postJson } from "../api/controlApi";
import { ParameterField } from "./ParameterField";
export function LayerCard({
layer,
index,
shaders,
expanded,
isDragging,
isDropTarget,
onToggleExpanded,
onDragStart,
onDragEnd,
onDragOver,
onDrop,
onRemove,
onLayerParameterChange,
}) {
return (
<div
className={`layer-card${expanded ? " layer-card--expanded" : ""}${isDragging ? " layer-card--dragging" : ""}${isDropTarget ? " layer-card--drop-target" : ""}`}
onDragOver={(event) => {
event.preventDefault();
onDragOver(layer.id);
}}
onDrop={(event) => {
event.preventDefault();
onDrop(event, layer.id, index);
}}
>
<div className="layer-card__header">
<div className="layer-card__meta">
<button
type="button"
className="layer-card__drag-handle"
title="Drag to reorder"
aria-label="Drag to reorder"
draggable
onDragStart={(event) => {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", layer.id);
event.stopPropagation();
onDragStart(layer.id);
}}
onDragEnd={(event) => {
event.stopPropagation();
onDragEnd();
}}
>
<GripVertical size={16} strokeWidth={1.75} />
</button>
<span className="layer-card__index">{index + 1}</span>
<button type="button" className="layer-card__title" onClick={() => onToggleExpanded(layer.id)}>
{layer.shaderName}
</button>
</div>
<div className="layer-card__actions">
<label className="toggle toggle--compact">
<input
type="checkbox"
checked={Boolean(layer.bypass)}
onChange={(event) =>
postJson("/api/layers/set-bypass", {
layerId: layer.id,
bypass: event.target.checked,
})
}
/>
<span>Bypass</span>
</label>
<button type="button" onClick={() => onToggleExpanded(layer.id)}>
{expanded ? "Hide" : "Controls"}
</button>
<button
type="button"
className="icon-button"
title="Remove layer"
aria-label="Remove layer"
onClick={() => onRemove(layer.id)}
>
<Trash2 size={16} strokeWidth={1.75} />
</button>
</div>
</div>
{expanded ? (
<div className="layer-card__body">
<div className="layer-card__field">
<label htmlFor={`shader-${layer.id}`}>Shader</label>
<select
id={`shader-${layer.id}`}
value={layer.shaderId}
onChange={(event) =>
postJson("/api/layers/set-shader", {
layerId: layer.id,
shaderId: event.target.value,
})
}
>
{shaders.map((shader) => (
<option key={shader.id} value={shader.id}>
{shader.name}
</option>
))}
</select>
</div>
{layer.temporal?.enabled ? (
<div className="layer-card__field">
<label>Temporal</label>
<div className="muted">
{layer.temporal.historySource} history, requested {layer.temporal.requestedHistoryLength} frame{layer.temporal.requestedHistoryLength === 1 ? "" : "s"}, using {layer.temporal.effectiveHistoryLength}
</div>
</div>
) : (
<div className="layer-card__field">
<label>Temporal</label>
<div className="muted">Stateless shader</div>
</div>
)}
<div className="layer-card__subheader">
<h3>Parameters</h3>
<button
type="button"
disabled={layer.parameters.length === 0}
onClick={() => postJson("/api/layers/reset-parameters", { layerId: layer.id })}
>
Reset
</button>
</div>
{layer.parameters.length > 0 ? (
<div className="parameter-grid">
{layer.parameters.map((parameter) => (
<ParameterField
key={`${layer.id}:${parameter.id}`}
parameter={parameter}
onParameterChange={(parameterId, value) => onLayerParameterChange(layer.id, parameterId, value)}
/>
))}
</div>
) : (
<p className="muted">This shader does not expose any user parameters.</p>
)}
</div>
) : null}
</div>
);
}