Control ui adjsutments
This commit is contained in:
@@ -23,7 +23,6 @@ function AppFooter() {
|
||||
|
||||
function App() {
|
||||
const [appState, setAppState] = useRuntimeState();
|
||||
const [pendingShaderId, setPendingShaderId] = useState("");
|
||||
const [presetName, setPresetName] = useState("");
|
||||
const [selectedPresetName, setSelectedPresetName] = useState("");
|
||||
const [expandedLayerIds, setExpandedLayerIds] = useState([]);
|
||||
@@ -38,14 +37,6 @@ function App() {
|
||||
const app = appState?.app ?? {};
|
||||
const stackPresets = appState?.stackPresets ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingShaderId && shaders.length > 0) {
|
||||
setPendingShaderId(shaders[0].id);
|
||||
} else if (pendingShaderId && !shaders.some((shader) => shader.id === pendingShaderId)) {
|
||||
setPendingShaderId(shaders[0]?.id ?? "");
|
||||
}
|
||||
}, [pendingShaderId, shaders]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedPresetName && stackPresets.length > 0) {
|
||||
setSelectedPresetName(stackPresets[0]);
|
||||
@@ -123,12 +114,10 @@ function App() {
|
||||
dropTargetLayerId={dropTargetLayerId}
|
||||
expandedLayerIds={expandedLayerIds}
|
||||
layers={layers}
|
||||
pendingShaderId={pendingShaderId}
|
||||
setAppState={setAppState}
|
||||
setDragLayerId={setDragLayerId}
|
||||
setDropTargetLayerId={setDropTargetLayerId}
|
||||
setExpandedLayerIds={setExpandedLayerIds}
|
||||
setPendingShaderId={setPendingShaderId}
|
||||
shaders={shaders}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GripVertical, Trash2 } from "lucide-react";
|
||||
import { EyeOff, GripVertical, RotateCcw, SlidersHorizontal, Trash2 } from "lucide-react";
|
||||
|
||||
import { postJson } from "../api/controlApi";
|
||||
import { ParameterField } from "./ParameterField";
|
||||
@@ -71,11 +71,13 @@ export function LayerCard({
|
||||
})
|
||||
}
|
||||
/>
|
||||
<EyeOff size={15} strokeWidth={1.8} aria-hidden="true" />
|
||||
<span>Bypass</span>
|
||||
</label>
|
||||
|
||||
<button type="button" onClick={() => onToggleExpanded(layer.id)}>
|
||||
{expanded ? "Hide" : "Controls"}
|
||||
<button type="button" className="button-with-icon" onClick={() => onToggleExpanded(layer.id)}>
|
||||
<SlidersHorizontal size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||
<span>{expanded ? "Hide" : "Controls"}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -116,10 +118,12 @@ export function LayerCard({
|
||||
<h3>Parameters</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="button-with-icon"
|
||||
disabled={layer.parameters.length === 0}
|
||||
onClick={() => postJson("/api/layers/reset-parameters", { layerId: layer.id })}
|
||||
>
|
||||
Reset
|
||||
<RotateCcw size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,12 +18,10 @@ export function LayerStack({
|
||||
dropTargetLayerId,
|
||||
expandedLayerIds,
|
||||
layers,
|
||||
pendingShaderId,
|
||||
setAppState,
|
||||
setDragLayerId,
|
||||
setDropTargetLayerId,
|
||||
setExpandedLayerIds,
|
||||
setPendingShaderId,
|
||||
shaders,
|
||||
}) {
|
||||
const expandedSet = new Set(expandedLayerIds);
|
||||
@@ -118,27 +116,13 @@ export function LayerStack({
|
||||
<span className="layer-card__index">+</span>
|
||||
<div className="layer-card__title layer-card__title--static">Add Layer</div>
|
||||
</div>
|
||||
<div className="layer-card__actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!pendingShaderId}
|
||||
onClick={() => {
|
||||
if (pendingShaderId) {
|
||||
postJson("/api/layers/add", { shaderId: pendingShaderId });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="layer-card__body">
|
||||
<div className="layer-card__field">
|
||||
<ShaderPicker
|
||||
id="add-layer"
|
||||
shaders={shaders}
|
||||
value={pendingShaderId}
|
||||
onChange={setPendingShaderId}
|
||||
onAdd={(shaderId) => postJson("/api/layers/add", { shaderId })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Wheel from "@uiw/react-color-wheel";
|
||||
import { hsvaToRgba, rgbaToHsva } from "@uiw/color-convert";
|
||||
import { Copy, RotateCcw } from "lucide-react";
|
||||
import { Copy, RotateCcw, Zap } from "lucide-react";
|
||||
|
||||
import { useThrottledParameterValue } from "../hooks/useThrottledParameterValue";
|
||||
import { ParameterValueDisplay } from "./ParameterValueDisplay";
|
||||
@@ -325,10 +325,11 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
{header}
|
||||
<button
|
||||
type="button"
|
||||
className="parameter__trigger"
|
||||
className="button-with-icon parameter__trigger"
|
||||
onClick={() => sendValue(triggerCount + 1)}
|
||||
>
|
||||
Trigger
|
||||
<Zap size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||
<span>Trigger</span>
|
||||
</button>
|
||||
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
|
||||
</section>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { ChevronDown, Search } from "lucide-react";
|
||||
import Fuse from "fuse.js";
|
||||
import { Plus, Search } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
function matchesShader(shader, query) {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
if (!normalizedQuery) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [shader.name, shader.id, shader.category, shader.description, shader.error]
|
||||
.filter(Boolean)
|
||||
.some((value) => value.toLowerCase().includes(normalizedQuery));
|
||||
}
|
||||
const shaderSearchOptions = {
|
||||
threshold: 0.38,
|
||||
ignoreLocation: true,
|
||||
minMatchCharLength: 2,
|
||||
keys: [
|
||||
{ name: "name", weight: 0.45 },
|
||||
{ name: "id", weight: 0.25 },
|
||||
{ name: "category", weight: 0.15 },
|
||||
{ name: "description", weight: 0.1 },
|
||||
{ name: "error", weight: 0.05 },
|
||||
],
|
||||
};
|
||||
|
||||
function shaderSummary(shader) {
|
||||
if (!shader) {
|
||||
@@ -37,86 +40,78 @@ function ShaderOptionContent({ shader }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ShaderPicker({ id, label = "Shader", shaders, value, onChange }) {
|
||||
export function ShaderPicker({ id, label = "Shader", shaders, onAdd }) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const filteredShaders = useMemo(
|
||||
() => shaders.filter((shader) => matchesShader(shader, query)),
|
||||
[query, shaders],
|
||||
const shaderSearch = useMemo(
|
||||
() => new Fuse(shaders, shaderSearchOptions),
|
||||
[shaders],
|
||||
);
|
||||
|
||||
const selectedShader = shaders.find((shader) => shader.id === value);
|
||||
const filteredShaders = useMemo(
|
||||
() => {
|
||||
const normalizedQuery = query.trim();
|
||||
if (!normalizedQuery) {
|
||||
return shaders;
|
||||
}
|
||||
|
||||
return shaderSearch.search(normalizedQuery).map((result) => result.item);
|
||||
},
|
||||
[query, shaderSearch, shaders],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="shader-picker">
|
||||
<div className="shader-picker__topline">
|
||||
<label id={`${id}-label`}>{label}</label>
|
||||
{selectedShader ? <span className="shader-picker__selected">{selectedShader.name}</span> : null}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="shader-picker__trigger"
|
||||
aria-labelledby={`${id}-label`}
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
>
|
||||
<span>
|
||||
<span className="shader-picker__option-head">
|
||||
<span className="shader-picker__name">{selectedShader?.name ?? "Choose shader"}</span>
|
||||
{selectedShader?.available === false ? (
|
||||
<span className="shader-picker__category shader-picker__category--error">Error</span>
|
||||
) : selectedShader?.category ? (
|
||||
<span className="shader-picker__category">{selectedShader.category}</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="shader-picker__meta">{shaderSummary(selectedShader)}</span>
|
||||
</span>
|
||||
<ChevronDown size={16} strokeWidth={1.75} aria-hidden="true" />
|
||||
</button>
|
||||
<div className="shader-picker__popover">
|
||||
<div className="shader-picker__search">
|
||||
<Search size={16} strokeWidth={1.75} aria-hidden="true" />
|
||||
<input
|
||||
id={`${id}-search`}
|
||||
type="text"
|
||||
value={query}
|
||||
placeholder="Search shaders"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{open ? (
|
||||
<div className="shader-picker__popover">
|
||||
<div className="shader-picker__search">
|
||||
<Search size={16} strokeWidth={1.75} aria-hidden="true" />
|
||||
<input
|
||||
id={`${id}-search`}
|
||||
type="text"
|
||||
value={query}
|
||||
placeholder="Search shaders"
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="shader-picker__list" role="listbox" aria-label={label}>
|
||||
{filteredShaders.length > 0 ? (
|
||||
filteredShaders.map((shader) => (
|
||||
<div className="shader-picker__list" role="list" aria-labelledby={`${id}-label`}>
|
||||
{filteredShaders.length > 0 ? (
|
||||
filteredShaders.map((shader) => (
|
||||
<div
|
||||
key={shader.id}
|
||||
className={`shader-picker__option${shader.available === false ? " shader-picker__option--unavailable" : ""}`}
|
||||
role="listitem"
|
||||
>
|
||||
<span className="shader-picker__option-copy">
|
||||
<ShaderOptionContent shader={shader} />
|
||||
</span>
|
||||
<button
|
||||
key={shader.id}
|
||||
type="button"
|
||||
className={`shader-picker__option${shader.id === value ? " shader-picker__option--selected" : ""}${shader.available === false ? " shader-picker__option--unavailable" : ""}`}
|
||||
role="option"
|
||||
aria-selected={shader.id === value}
|
||||
className="icon-button shader-picker__add"
|
||||
title={`Add ${shader.name}`}
|
||||
aria-label={`Add ${shader.name}`}
|
||||
disabled={shader.available === false}
|
||||
onClick={() => {
|
||||
if (shader.available === false) {
|
||||
return;
|
||||
}
|
||||
onChange(shader.id);
|
||||
setOpen(false);
|
||||
onAdd(shader.id);
|
||||
setQuery("");
|
||||
}}
|
||||
>
|
||||
<ShaderOptionContent shader={shader} />
|
||||
<Plus size={16} strokeWidth={1.9} />
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="shader-picker__empty">No shaders found</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="shader-picker__empty">No shaders found</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { FolderOpen, RefreshCw, Save } from "lucide-react";
|
||||
|
||||
import { postJson } from "../api/controlApi";
|
||||
|
||||
export function StackPresetToolbar({
|
||||
@@ -14,8 +16,13 @@ export function StackPresetToolbar({
|
||||
<h3>Stack presets</h3>
|
||||
<p className="muted">Save or recall the current layer chain.</p>
|
||||
</div>
|
||||
<button type="button" className="stack-panel__reload" onClick={() => postJson("/api/reload", {})}>
|
||||
Reload shader
|
||||
<button
|
||||
type="button"
|
||||
className="button-with-icon stack-panel__reload"
|
||||
onClick={() => postJson("/api/reload", {})}
|
||||
>
|
||||
<RefreshCw size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||
<span>Reload shader</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +39,7 @@ export function StackPresetToolbar({
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="button-with-icon"
|
||||
disabled={!presetName.trim()}
|
||||
onClick={() => {
|
||||
const trimmedName = presetName.trim();
|
||||
@@ -44,7 +52,8 @@ export function StackPresetToolbar({
|
||||
);
|
||||
}}
|
||||
>
|
||||
Save
|
||||
<Save size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||
<span>Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,6 +75,7 @@ export function StackPresetToolbar({
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="button-with-icon"
|
||||
disabled={!selectedPresetName}
|
||||
onClick={() => {
|
||||
if (selectedPresetName) {
|
||||
@@ -73,7 +83,8 @@ export function StackPresetToolbar({
|
||||
}
|
||||
}}
|
||||
>
|
||||
Recall
|
||||
<FolderOpen size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||
<span>Recall</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,6 +156,17 @@ button:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.button-with-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.button-with-icon svg {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
input:disabled,
|
||||
select:disabled {
|
||||
@@ -746,7 +757,7 @@ pre {
|
||||
.shader-picker__list {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
max-height: 250px;
|
||||
max-height: min(52vh, 520px);
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--app-border);
|
||||
@@ -754,22 +765,22 @@ pre {
|
||||
}
|
||||
|
||||
.shader-picker__option {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
min-height: 4.25rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border: 1px solid var(--app-border);
|
||||
text-align: left;
|
||||
background: #182232;
|
||||
border-color: var(--app-border);
|
||||
align-content: start;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.shader-picker__option--selected {
|
||||
background: #203b54;
|
||||
border-color: var(--app-primary);
|
||||
box-shadow: inset 0 0 0 1px var(--app-primary-soft);
|
||||
.shader-picker__option-copy {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shader-picker__option--unavailable {
|
||||
@@ -778,16 +789,16 @@ pre {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.shader-picker__option:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.shader-picker__option--unavailable .shader-picker__name,
|
||||
.shader-picker__option--unavailable .shader-picker__meta {
|
||||
color: #ffd0cf;
|
||||
}
|
||||
|
||||
.shader-picker__add {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.shader-picker__name,
|
||||
.shader-picker__meta {
|
||||
min-width: 0;
|
||||
@@ -1040,6 +1051,11 @@ pre {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.toggle svg {
|
||||
flex: 0 0 auto;
|
||||
color: var(--app-muted);
|
||||
}
|
||||
|
||||
.toggle--compact {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user