@pixerate/editor-react 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,667 @@
1
+ // src/parameters/useParameters.ts
2
+ import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
+ import {
4
+ ParametersController,
5
+ createParameterCommands
6
+ } from "@pixerate/editor/parameters";
7
+ function useParametersController(options) {
8
+ const ref = useRef(null);
9
+ if (!ref.current) ref.current = new ParametersController(options);
10
+ const controller = ref.current;
11
+ const schema = options.schema;
12
+ const first = useRef(true);
13
+ useEffect(() => {
14
+ if (first.current) {
15
+ first.current = false;
16
+ return;
17
+ }
18
+ controller.setSchema(schema);
19
+ }, [controller, schema]);
20
+ return controller;
21
+ }
22
+ function useParametersState(controller) {
23
+ return useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
24
+ }
25
+ function useParameters(controllerOrOptions) {
26
+ const own = useRef(null);
27
+ let controller;
28
+ if (controllerOrOptions instanceof ParametersController) controller = controllerOrOptions;
29
+ else {
30
+ if (!own.current) own.current = new ParametersController(controllerOrOptions);
31
+ controller = own.current;
32
+ }
33
+ const state = useParametersState(controller);
34
+ const actions = useMemo(
35
+ () => ({
36
+ setValue: (id, value) => controller.setValue(id, value),
37
+ undo: () => void controller.undo(),
38
+ redo: () => void controller.redo(),
39
+ reset: (ids) => void controller.reset(ids),
40
+ randomize: (ids) => void controller.randomize({ ids })
41
+ }),
42
+ [controller]
43
+ );
44
+ return { controller, state, values: state.values, ...actions };
45
+ }
46
+ function useParameterCommands(registry, controller, options = {}) {
47
+ const { prefix, group, scope } = options;
48
+ useEffect(() => {
49
+ if (!registry) return;
50
+ return registry.register(createParameterCommands(controller, { prefix, group, scope }));
51
+ }, [registry, controller, prefix, group, scope]);
52
+ }
53
+
54
+ // src/parameters/fields.tsx
55
+ import { useRef as useRef2 } from "react";
56
+ import {
57
+ choiceOptionLabel,
58
+ formatParameterNumber,
59
+ getChoiceOptions,
60
+ isSliderField,
61
+ normalizeHexColor,
62
+ parameterLabel,
63
+ parameterStep,
64
+ parseParameterNumberInput,
65
+ clampParameterNumber,
66
+ vectorComponentField,
67
+ vectorSize
68
+ } from "@pixerate/editor/parameters";
69
+ import { jsx, jsxs } from "react/jsx-runtime";
70
+ var parameterInputClass = "h-7 w-full min-w-0 rounded-md border border-input bg-background px-2 text-xs text-foreground tabular-nums focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50";
71
+ var parameterButtonClass = "inline-flex h-7 items-center justify-center gap-1 rounded-md border border-input bg-background px-2 text-xs text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50";
72
+ var rangeClass = "h-4 w-full min-w-0 flex-1 cursor-pointer accent-primary disabled:cursor-not-allowed disabled:opacity-50";
73
+ function ParameterCommitInput({ value, onCommit, multiline, className, onKeyDown, ...rest }) {
74
+ const commit = (el) => {
75
+ if (el.value !== value) onCommit(el.value, el);
76
+ };
77
+ const keyDown = (e) => {
78
+ onKeyDown?.(e);
79
+ if (e.defaultPrevented) return;
80
+ if (e.key === "Enter" && (!multiline || e.metaKey || e.ctrlKey)) {
81
+ e.preventDefault();
82
+ commit(e.currentTarget);
83
+ }
84
+ if (e.key === "Escape") e.currentTarget.value = value;
85
+ };
86
+ const props = {
87
+ defaultValue: value,
88
+ className: className ?? parameterInputClass,
89
+ onBlur: (e) => commit(e.currentTarget),
90
+ onKeyDown: keyDown,
91
+ ...rest
92
+ };
93
+ return multiline ? /* @__PURE__ */ jsx("textarea", { ...props, className: `${props.className} h-auto min-h-[3.5rem] py-1` }, value) : /* @__PURE__ */ jsx("input", { type: "text", ...props }, value);
94
+ }
95
+ var plainNumber = (field, value) => formatParameterNumber({ ...field, unit: void 0 }, value);
96
+ function ParameterNumberInput({
97
+ field,
98
+ value,
99
+ onChange,
100
+ disabled,
101
+ id,
102
+ "aria-label": ariaLabel
103
+ }) {
104
+ return /* @__PURE__ */ jsxs("div", { className: "relative min-w-0 flex-1", children: [
105
+ /* @__PURE__ */ jsx(
106
+ ParameterCommitInput,
107
+ {
108
+ id,
109
+ inputMode: "decimal",
110
+ "aria-label": ariaLabel ?? parameterLabel(field),
111
+ value: plainNumber(field, value),
112
+ disabled: disabled || field.readOnly,
113
+ className: `${parameterInputClass} ${field.unit ? "pr-6" : ""}`,
114
+ onCommit: (text, input) => {
115
+ const n = parseParameterNumberInput(field, text);
116
+ if (n === null || n === value) input.value = plainNumber(field, value);
117
+ else onChange(n);
118
+ },
119
+ onKeyDown: (e) => {
120
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
121
+ e.preventDefault();
122
+ const step = parameterStep(field) * (e.shiftKey ? 10 : 1);
123
+ onChange(clampParameterNumber(field, value + (e.key === "ArrowUp" ? step : -step)));
124
+ }
125
+ }
126
+ ),
127
+ field.unit && /* @__PURE__ */ jsx("span", { className: "pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground", children: field.unit })
128
+ ] });
129
+ }
130
+ function ParameterSlider(props) {
131
+ const { field, value, onChange, onGestureStart, onGestureEnd, disabled } = props;
132
+ const dragging = useRef2(false);
133
+ const end = () => {
134
+ if (!dragging.current) return;
135
+ dragging.current = false;
136
+ onGestureEnd?.();
137
+ };
138
+ if (!isSliderField(field)) return /* @__PURE__ */ jsx(ParameterNumberInput, { ...props });
139
+ const step = parameterStep(field);
140
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
141
+ /* @__PURE__ */ jsx(
142
+ "input",
143
+ {
144
+ type: "range",
145
+ "aria-label": parameterLabel(field),
146
+ "aria-valuetext": formatParameterNumber(field, value),
147
+ min: field.min,
148
+ max: field.max,
149
+ step,
150
+ value,
151
+ disabled: disabled || field.readOnly,
152
+ className: rangeClass,
153
+ onPointerDown: () => {
154
+ if (dragging.current) return;
155
+ dragging.current = true;
156
+ onGestureStart?.();
157
+ },
158
+ onPointerUp: end,
159
+ onPointerCancel: end,
160
+ onBlur: end,
161
+ onChange: (e) => onChange(clampParameterNumber(field, Number(e.target.value)))
162
+ }
163
+ ),
164
+ /* @__PURE__ */ jsx("div", { className: "w-20 shrink-0", children: /* @__PURE__ */ jsx(ParameterNumberInput, { ...props, id: props.id, "aria-label": `${parameterLabel(field)} value` }) })
165
+ ] });
166
+ }
167
+ function ParameterToggle({ field, value, onChange, disabled, id }) {
168
+ return /* @__PURE__ */ jsx(
169
+ "button",
170
+ {
171
+ type: "button",
172
+ role: "switch",
173
+ id,
174
+ "aria-checked": value,
175
+ "aria-label": parameterLabel(field),
176
+ disabled: disabled || field.readOnly,
177
+ onClick: () => onChange(!value),
178
+ className: `relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:opacity-50 ${value ? "bg-primary" : "bg-muted"}`,
179
+ children: /* @__PURE__ */ jsx(
180
+ "span",
181
+ {
182
+ className: `pointer-events-none block h-4 w-4 rounded-full bg-background shadow transition-transform ${value ? "translate-x-4" : "translate-x-0.5"}`
183
+ }
184
+ )
185
+ }
186
+ );
187
+ }
188
+ function ParameterSelect({ field, value, onChange, disabled, id }) {
189
+ const options = getChoiceOptions(field);
190
+ const off = disabled || field.readOnly;
191
+ if (field.control === "segmented") {
192
+ return /* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-label": parameterLabel(field), className: "inline-flex flex-wrap rounded-md border border-input bg-muted p-0.5", children: options.map((o) => /* @__PURE__ */ jsx(
193
+ "button",
194
+ {
195
+ type: "button",
196
+ role: "radio",
197
+ "aria-checked": o.value === value,
198
+ title: o.description,
199
+ disabled: off,
200
+ onClick: () => onChange(o.value),
201
+ className: `rounded px-2 py-0.5 text-xs disabled:opacity-50 ${o.value === value ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`,
202
+ children: choiceOptionLabel(o)
203
+ },
204
+ String(o.value)
205
+ )) });
206
+ }
207
+ const index = options.findIndex((o) => o.value === value);
208
+ return /* @__PURE__ */ jsx(
209
+ "select",
210
+ {
211
+ id,
212
+ "aria-label": parameterLabel(field),
213
+ value: String(index),
214
+ disabled: off,
215
+ onChange: (e) => {
216
+ const option = options[Number(e.target.value)];
217
+ if (option) onChange(option.value);
218
+ },
219
+ className: parameterInputClass,
220
+ children: options.map((o, i) => /* @__PURE__ */ jsx("option", { value: String(i), title: o.description, children: choiceOptionLabel(o) }, String(o.value)))
221
+ }
222
+ );
223
+ }
224
+ function ParameterColor({ field, value, onChange, onGestureStart, onGestureEnd, disabled, id }) {
225
+ const picking = useRef2(false);
226
+ const alpha = !!field.alpha;
227
+ const hex = normalizeHexColor(value, true) ?? "#000000ff";
228
+ const rgb = hex.slice(0, 7);
229
+ const a = hex.slice(7);
230
+ const off = disabled || field.readOnly;
231
+ const start = () => {
232
+ if (picking.current) return;
233
+ picking.current = true;
234
+ onGestureStart?.();
235
+ };
236
+ const end = () => {
237
+ if (!picking.current) return;
238
+ picking.current = false;
239
+ onGestureEnd?.();
240
+ };
241
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
242
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
243
+ /* @__PURE__ */ jsxs("span", { className: "relative h-7 w-9 shrink-0 overflow-hidden rounded-md border border-input bg-[repeating-conic-gradient(#d4d4d8_0_25%,#fff_0_50%)] bg-[length:8px_8px]", children: [
244
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0", style: { background: alpha ? hex : rgb } }),
245
+ /* @__PURE__ */ jsx(
246
+ "input",
247
+ {
248
+ type: "color",
249
+ "aria-label": parameterLabel(field),
250
+ value: rgb,
251
+ disabled: off,
252
+ className: "absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed",
253
+ onChange: (e) => {
254
+ start();
255
+ onChange(alpha ? `${e.target.value}${a}` : e.target.value);
256
+ },
257
+ onBlur: end
258
+ }
259
+ )
260
+ ] }),
261
+ /* @__PURE__ */ jsx(
262
+ ParameterCommitInput,
263
+ {
264
+ id,
265
+ "aria-label": `${parameterLabel(field)} hex`,
266
+ value: alpha ? hex : rgb,
267
+ disabled: off,
268
+ spellCheck: false,
269
+ className: `${parameterInputClass} font-mono`,
270
+ onCommit: (text) => {
271
+ const next = normalizeHexColor(text, alpha);
272
+ if (next) onChange(next);
273
+ }
274
+ }
275
+ )
276
+ ] }),
277
+ alpha && /* @__PURE__ */ jsx(
278
+ "input",
279
+ {
280
+ type: "range",
281
+ "aria-label": `${parameterLabel(field)} opacity`,
282
+ min: 0,
283
+ max: 255,
284
+ step: 1,
285
+ value: parseInt(a, 16),
286
+ disabled: off,
287
+ className: rangeClass,
288
+ onPointerDown: start,
289
+ onPointerUp: end,
290
+ onPointerCancel: end,
291
+ onBlur: end,
292
+ onChange: (e) => onChange(`${rgb}${Number(e.target.value).toString(16).padStart(2, "0")}`)
293
+ }
294
+ ),
295
+ field.swatches && field.swatches.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: field.swatches.map((s) => {
296
+ const swatch = normalizeHexColor(s, alpha);
297
+ if (!swatch) return null;
298
+ return /* @__PURE__ */ jsx(
299
+ "button",
300
+ {
301
+ type: "button",
302
+ title: swatch,
303
+ "aria-label": `${parameterLabel(field)} ${swatch}`,
304
+ "aria-pressed": swatch === (alpha ? hex : rgb),
305
+ disabled: off,
306
+ onClick: () => onChange(swatch),
307
+ className: "h-5 w-5 rounded border border-input aria-pressed:ring-1 aria-pressed:ring-primary",
308
+ style: { background: swatch }
309
+ },
310
+ s
311
+ );
312
+ }) })
313
+ ] });
314
+ }
315
+ function ParameterVector({ field, value, onChange, disabled, id }) {
316
+ const size = vectorSize(field);
317
+ return /* @__PURE__ */ jsx("div", { className: "grid gap-1.5", style: { gridTemplateColumns: `repeat(${size}, minmax(0, 1fr))` }, children: Array.from({ length: size }, (_, i) => {
318
+ const component = vectorComponentField(field, i);
319
+ return /* @__PURE__ */ jsxs("label", { className: "flex min-w-0 items-center gap-1", children: [
320
+ /* @__PURE__ */ jsx("span", { className: "w-3 shrink-0 text-[10px] uppercase text-muted-foreground", children: component.label }),
321
+ /* @__PURE__ */ jsx(
322
+ ParameterNumberInput,
323
+ {
324
+ field: component,
325
+ value: value[i] ?? 0,
326
+ id: i === 0 ? id : void 0,
327
+ "aria-label": `${parameterLabel(field)} ${component.label}`,
328
+ disabled: disabled || field.readOnly,
329
+ onChange: (n) => onChange(Array.from({ length: size }, (_2, j) => j === i ? n : value[j] ?? 0))
330
+ }
331
+ )
332
+ ] }, i);
333
+ }) });
334
+ }
335
+ function ParameterImage({ field, value, onChange, disabled, id, resolveFile }) {
336
+ const off = disabled || field.readOnly;
337
+ const name = parameterLabel(field);
338
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
339
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
340
+ /* @__PURE__ */ jsx("div", { className: "flex h-12 w-16 shrink-0 items-center justify-center overflow-hidden rounded-md border border-input bg-muted", children: value ? /* @__PURE__ */ jsx("img", { src: value, alt: name, className: "h-full w-full object-cover" }) : /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground", children: "None" }) }),
341
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
342
+ /* @__PURE__ */ jsx(
343
+ ParameterCommitInput,
344
+ {
345
+ id,
346
+ "aria-label": `${name} URL`,
347
+ placeholder: "Image URL",
348
+ value: value.startsWith("data:") ? "" : value,
349
+ disabled: off,
350
+ onCommit: (text) => onChange(text.trim())
351
+ }
352
+ ),
353
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
354
+ resolveFile && /* @__PURE__ */ jsxs("label", { className: `${parameterButtonClass} cursor-pointer ${off ? "pointer-events-none opacity-50" : ""}`, children: [
355
+ "Choose file\u2026",
356
+ /* @__PURE__ */ jsx(
357
+ "input",
358
+ {
359
+ type: "file",
360
+ className: "sr-only",
361
+ accept: field.accept ?? "image/*",
362
+ disabled: off,
363
+ "aria-label": `${name} file`,
364
+ onChange: async (e) => {
365
+ const file = e.target.files?.[0];
366
+ e.target.value = "";
367
+ if (file) onChange(await resolveFile(file));
368
+ }
369
+ }
370
+ )
371
+ ] }),
372
+ value && /* @__PURE__ */ jsx("button", { type: "button", className: parameterButtonClass, disabled: off, onClick: () => onChange(""), "aria-label": `Clear ${name}`, children: "Clear" })
373
+ ] })
374
+ ] })
375
+ ] }),
376
+ field.options && field.options.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: field.options.map((o) => /* @__PURE__ */ jsx(
377
+ "button",
378
+ {
379
+ type: "button",
380
+ title: o.label ?? o.url,
381
+ "aria-label": o.label ?? o.url,
382
+ "aria-pressed": o.url === value,
383
+ disabled: off,
384
+ onClick: () => onChange(o.url),
385
+ className: "h-9 w-12 overflow-hidden rounded border border-input aria-pressed:ring-2 aria-pressed:ring-primary",
386
+ children: /* @__PURE__ */ jsx("img", { src: o.url, alt: "", className: "h-full w-full object-cover" })
387
+ },
388
+ o.url
389
+ )) })
390
+ ] });
391
+ }
392
+ function ParameterText({ field, value, onChange, disabled, id }) {
393
+ return /* @__PURE__ */ jsx(
394
+ ParameterCommitInput,
395
+ {
396
+ id,
397
+ "aria-label": parameterLabel(field),
398
+ multiline: field.multiline,
399
+ placeholder: field.placeholder,
400
+ maxLength: field.maxLength,
401
+ value,
402
+ disabled: disabled || field.readOnly,
403
+ onCommit: onChange
404
+ }
405
+ );
406
+ }
407
+ function KeyframeDiamond({ state, onToggle, label, disabled, className = "" }) {
408
+ const keyed = state === "keyed";
409
+ return /* @__PURE__ */ jsx(
410
+ "button",
411
+ {
412
+ type: "button",
413
+ "data-key-state": state,
414
+ "aria-pressed": keyed,
415
+ "aria-label": `${keyed ? "Remove" : "Add"} keyframe for ${label}`,
416
+ title: keyed ? "Remove keyframe here" : "Add keyframe at the playhead",
417
+ disabled,
418
+ onClick: onToggle,
419
+ className: `inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:opacity-40 ${state === "none" ? "text-muted-foreground hover:text-foreground" : "text-amber-500"} ${className}`,
420
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 10 10", className: "h-2.5 w-2.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
421
+ "path",
422
+ {
423
+ d: "M5 0.8 9.2 5 5 9.2 0.8 5Z",
424
+ fill: keyed ? "currentColor" : "none",
425
+ stroke: "currentColor",
426
+ strokeWidth: "1.3",
427
+ opacity: state === "animated" ? 0.7 : 1
428
+ }
429
+ ) })
430
+ }
431
+ );
432
+ }
433
+ function ParameterFieldControl({ field, value, onChange, resolveFile, ...rest }) {
434
+ const set = onChange;
435
+ switch (field.type) {
436
+ case "number":
437
+ case "int":
438
+ return /* @__PURE__ */ jsx(ParameterSlider, { field, value, onChange: set, ...rest });
439
+ case "boolean":
440
+ return /* @__PURE__ */ jsx(ParameterToggle, { field, value, onChange: set, ...rest });
441
+ case "choice":
442
+ return /* @__PURE__ */ jsx(ParameterSelect, { field, value, onChange: set, ...rest });
443
+ case "color":
444
+ return /* @__PURE__ */ jsx(ParameterColor, { field, value, onChange: set, ...rest });
445
+ case "vector":
446
+ return /* @__PURE__ */ jsx(ParameterVector, { field, value, onChange: set, ...rest });
447
+ case "image":
448
+ return /* @__PURE__ */ jsx(ParameterImage, { field, value, onChange: set, resolveFile, ...rest });
449
+ case "text":
450
+ return /* @__PURE__ */ jsx(ParameterText, { field, value, onChange: set, ...rest });
451
+ }
452
+ }
453
+
454
+ // src/parameters/ParametersPanel.tsx
455
+ import { useId, useState } from "react";
456
+ import {
457
+ isParameterGroup,
458
+ parameterLabel as parameterLabel2,
459
+ parameterValuesEqual
460
+ } from "@pixerate/editor/parameters";
461
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
462
+ function FieldRow({
463
+ controller,
464
+ state,
465
+ field,
466
+ idBase,
467
+ showKeyframes,
468
+ showFieldReset,
469
+ resolveFile
470
+ }) {
471
+ const label = parameterLabel2(field);
472
+ const inputId = `${idBase}-${field.id}`;
473
+ const value = state.values[field.id];
474
+ const error = state.errors[field.id];
475
+ const changed = !parameterValuesEqual(value, state.defaults[field.id]);
476
+ return /* @__PURE__ */ jsxs2("div", { "data-parameter-id": field.id, "data-parameter-type": field.type, className: "space-y-1", children: [
477
+ /* @__PURE__ */ jsxs2("div", { className: "flex min-h-[1.25rem] items-center gap-1.5", children: [
478
+ showKeyframes && /* @__PURE__ */ jsx2(KeyframeDiamond, { state: state.keys[field.id] ?? "none", label, onToggle: () => controller.toggleKeyframe(field.id) }),
479
+ /* @__PURE__ */ jsx2("label", { htmlFor: inputId, className: "min-w-0 flex-1 truncate text-xs font-medium text-foreground", title: field.description, children: label }),
480
+ showFieldReset && changed && !field.readOnly && /* @__PURE__ */ jsx2(
481
+ "button",
482
+ {
483
+ type: "button",
484
+ className: "rounded px-1 text-[11px] text-muted-foreground hover:text-foreground",
485
+ title: `Reset to ${String(state.defaults[field.id])}`,
486
+ "aria-label": `Reset ${label}`,
487
+ onClick: () => controller.reset([field.id]),
488
+ children: "\u21BA"
489
+ }
490
+ )
491
+ ] }),
492
+ field.description && /* @__PURE__ */ jsx2("p", { className: "text-[11px] leading-snug text-muted-foreground", children: field.description }),
493
+ /* @__PURE__ */ jsx2(
494
+ ParameterFieldControl,
495
+ {
496
+ field,
497
+ value,
498
+ id: inputId,
499
+ resolveFile,
500
+ onChange: (v) => controller.setValue(field.id, v),
501
+ onGestureStart: () => controller.beginGesture(field.id),
502
+ onGestureEnd: () => controller.endGesture()
503
+ }
504
+ ),
505
+ error && /* @__PURE__ */ jsx2("p", { role: "alert", className: "text-[11px] text-destructive", children: error })
506
+ ] });
507
+ }
508
+ function PresetBar({ controller, state }) {
509
+ const [saving, setSaving] = useState(false);
510
+ const [name, setName] = useState("");
511
+ const [message, setMessage] = useState(null);
512
+ const active = state.presets.find((p) => p.name === state.activePreset);
513
+ const save = (e) => {
514
+ e.preventDefault();
515
+ const result = controller.savePreset(name);
516
+ if (result.ok) {
517
+ setSaving(false);
518
+ setName("");
519
+ setMessage(null);
520
+ } else setMessage(result.message);
521
+ };
522
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-1.5", "data-parameter-presets": "", children: [
523
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5", children: [
524
+ /* @__PURE__ */ jsxs2(
525
+ "select",
526
+ {
527
+ "aria-label": "Preset",
528
+ className: parameterInputClass,
529
+ value: state.activePreset ?? "",
530
+ onChange: (e) => e.target.value && controller.applyPreset(e.target.value),
531
+ children: [
532
+ /* @__PURE__ */ jsx2("option", { value: "", children: state.presets.length ? "Presets\u2026" : "No presets" }),
533
+ state.presets.map((p) => /* @__PURE__ */ jsx2("option", { value: p.name, title: p.description, children: p.name }, p.name))
534
+ ]
535
+ }
536
+ ),
537
+ /* @__PURE__ */ jsx2("button", { type: "button", className: parameterButtonClass, onClick: () => setSaving((s) => !s), "aria-expanded": saving, children: "Save\u2026" }),
538
+ active && !active.builtIn && /* @__PURE__ */ jsx2("button", { type: "button", className: parameterButtonClass, "aria-label": `Delete preset ${active.name}`, onClick: () => controller.deletePreset(active.name), children: "Delete" })
539
+ ] }),
540
+ saving && /* @__PURE__ */ jsxs2("form", { className: "flex items-center gap-1.5", onSubmit: save, children: [
541
+ /* @__PURE__ */ jsx2(
542
+ "input",
543
+ {
544
+ autoFocus: true,
545
+ "aria-label": "Preset name",
546
+ placeholder: "Preset name",
547
+ className: parameterInputClass,
548
+ value: name,
549
+ onChange: (e) => setName(e.target.value)
550
+ }
551
+ ),
552
+ /* @__PURE__ */ jsx2("button", { type: "submit", className: parameterButtonClass, children: "Save" })
553
+ ] }),
554
+ message && /* @__PURE__ */ jsx2("p", { role: "alert", className: "text-[11px] text-destructive", children: message })
555
+ ] });
556
+ }
557
+ function ParametersPanel({
558
+ controller,
559
+ title,
560
+ showToolbar = true,
561
+ showPresets = true,
562
+ showKeyframes,
563
+ showFieldReset = true,
564
+ resolveFile,
565
+ emptyMessage = "No parameters.",
566
+ className = ""
567
+ }) {
568
+ const state = useParametersState(controller);
569
+ const idBase = useId();
570
+ const keys = showKeyframes ?? state.keyframesEnabled;
571
+ const heading = title === void 0 ? state.schema.title : title;
572
+ const renderItems = (items) => items.map((item) => {
573
+ if (state.visible[item.id] === false) return null;
574
+ if (isParameterGroup(item)) return renderGroup(item);
575
+ return /* @__PURE__ */ jsx2(
576
+ FieldRow,
577
+ {
578
+ controller,
579
+ state,
580
+ field: item,
581
+ idBase,
582
+ showKeyframes: keys,
583
+ showFieldReset,
584
+ resolveFile
585
+ },
586
+ item.id
587
+ );
588
+ });
589
+ const renderGroup = (group) => {
590
+ const collapsed = !!state.collapsed[group.id];
591
+ const bodyId = `${idBase}-group-${group.id}`;
592
+ return /* @__PURE__ */ jsxs2("section", { "data-parameter-group": group.id, className: "rounded-md border border-border", children: [
593
+ /* @__PURE__ */ jsxs2(
594
+ "button",
595
+ {
596
+ type: "button",
597
+ "aria-expanded": !collapsed,
598
+ "aria-controls": bodyId,
599
+ onClick: () => controller.toggleCollapsed(group.id),
600
+ className: "flex w-full items-center gap-1.5 px-2 py-1.5 text-left text-xs font-semibold text-foreground hover:bg-muted",
601
+ children: [
602
+ /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", className: `inline-block text-[10px] text-muted-foreground transition-transform ${collapsed ? "" : "rotate-90"}`, children: "\u25B6" }),
603
+ parameterLabel2(group)
604
+ ]
605
+ }
606
+ ),
607
+ !collapsed && /* @__PURE__ */ jsxs2("div", { id: bodyId, className: "space-y-3 border-t border-border px-2 py-2", children: [
608
+ group.description && /* @__PURE__ */ jsx2("p", { className: "text-[11px] text-muted-foreground", children: group.description }),
609
+ renderItems(group.fields)
610
+ ] })
611
+ ] }, group.id);
612
+ };
613
+ return /* @__PURE__ */ jsxs2("div", { className: `space-y-3 text-foreground ${className}`, "data-parameters-panel": "", children: [
614
+ (heading || showToolbar) && /* @__PURE__ */ jsxs2("div", { className: "flex flex-wrap items-center gap-1.5", children: [
615
+ heading && /* @__PURE__ */ jsx2("h3", { className: "mr-auto text-sm font-semibold", children: heading }),
616
+ showToolbar && /* @__PURE__ */ jsxs2("div", { className: "flex flex-wrap gap-1", role: "toolbar", "aria-label": "Parameter actions", children: [
617
+ /* @__PURE__ */ jsx2(
618
+ "button",
619
+ {
620
+ type: "button",
621
+ className: parameterButtonClass,
622
+ disabled: !state.canUndo,
623
+ title: state.canUndo ? `Undo ${state.undoLabel}` : "Nothing to undo",
624
+ onClick: () => controller.undo(),
625
+ children: "Undo"
626
+ }
627
+ ),
628
+ /* @__PURE__ */ jsx2(
629
+ "button",
630
+ {
631
+ type: "button",
632
+ className: parameterButtonClass,
633
+ disabled: !state.canRedo,
634
+ title: state.canRedo ? `Redo ${state.redoLabel}` : "Nothing to redo",
635
+ onClick: () => controller.redo(),
636
+ children: "Redo"
637
+ }
638
+ ),
639
+ /* @__PURE__ */ jsx2("button", { type: "button", className: parameterButtonClass, disabled: !state.modified, onClick: () => controller.reset(), children: "Reset" }),
640
+ /* @__PURE__ */ jsx2("button", { type: "button", className: parameterButtonClass, onClick: () => controller.randomize(), children: "Randomize" })
641
+ ] })
642
+ ] }),
643
+ showPresets && /* @__PURE__ */ jsx2(PresetBar, { controller, state }),
644
+ state.fields.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: emptyMessage }) : renderItems(state.schema.fields)
645
+ ] });
646
+ }
647
+ export {
648
+ KeyframeDiamond,
649
+ ParameterColor,
650
+ ParameterCommitInput,
651
+ ParameterFieldControl,
652
+ ParameterImage,
653
+ ParameterNumberInput,
654
+ ParameterSelect,
655
+ ParameterSlider,
656
+ ParameterText,
657
+ ParameterToggle,
658
+ ParameterVector,
659
+ ParametersPanel,
660
+ parameterButtonClass,
661
+ parameterInputClass,
662
+ useParameterCommands,
663
+ useParameters,
664
+ useParametersController,
665
+ useParametersState
666
+ };
667
+ //# sourceMappingURL=index.js.map