@pixerate/editor-react 0.77.0 → 0.78.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,606 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/effects/index.ts
31
+ var effects_exports = {};
32
+ __export(effects_exports, {
33
+ EffectParameters: () => EffectParameters,
34
+ EffectThumbnail: () => EffectThumbnail,
35
+ EffectsBrowser: () => EffectsBrowser,
36
+ useEffectDescription: () => useEffectDescription,
37
+ useEffects: () => useEffects
38
+ });
39
+ module.exports = __toCommonJS(effects_exports);
40
+
41
+ // src/effects/useEffects.ts
42
+ var import_react = require("react");
43
+ function useEffects(host, provider) {
44
+ (0, import_react.useEffect)(() => {
45
+ if (!host || !provider) return;
46
+ return host.useEffects(provider);
47
+ }, [host, provider]);
48
+ }
49
+ function useEffectDescription(provider, effect) {
50
+ const [state, setState] = (0, import_react.useState)({ key: "", description: null, error: null });
51
+ const key = provider && effect ? `${provider.id}:${effect}` : "";
52
+ (0, import_react.useEffect)(() => {
53
+ if (!provider || !effect) return;
54
+ let live = true;
55
+ Promise.resolve().then(() => provider.describe(effect)).then(
56
+ (description) => live && setState({ key, description, error: null }),
57
+ (e) => live && setState({ key, description: null, error: e instanceof Error ? e.message : String(e) })
58
+ );
59
+ return () => {
60
+ live = false;
61
+ };
62
+ }, [provider, effect, key]);
63
+ return state.key === key ? { description: state.description, error: state.error } : { description: null, error: null };
64
+ }
65
+
66
+ // src/effects/EffectThumbnail.tsx
67
+ var import_react2 = require("react");
68
+ var import_effects = require("@pixerate/editor/effects");
69
+ var import_jsx_runtime = require("react/jsx-runtime");
70
+ function EffectThumbnail({ provider, effect, width = 96, height = 54, className = "" }) {
71
+ const ref = (0, import_react2.useRef)(null);
72
+ const [failed, setFailed] = (0, import_react2.useState)(effect.kind === "motion");
73
+ (0, import_react2.useEffect)(() => {
74
+ if (effect.kind === "motion") return;
75
+ let live = true;
76
+ (0, import_effects.renderEffectThumbnail)(provider, effect, { width, height }).then(
77
+ (pixels) => {
78
+ const ctx = live ? ref.current?.getContext("2d") : null;
79
+ if (!ctx || typeof ImageData === "undefined") return;
80
+ ctx.putImageData(new ImageData(new Uint8ClampedArray(pixels.data), pixels.width, pixels.height), 0, 0);
81
+ },
82
+ () => live && setFailed(true)
83
+ );
84
+ return () => {
85
+ live = false;
86
+ };
87
+ }, [provider, effect, width, height]);
88
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
89
+ "div",
90
+ {
91
+ className: `relative overflow-hidden rounded border border-border bg-[repeating-conic-gradient(#8884_0_25%,transparent_0_50%)] bg-[length:12px_12px] ${className}`,
92
+ style: { aspectRatio: `${width} / ${height}` },
93
+ "data-effect-thumbnail": effect.id,
94
+ children: failed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute inset-0 flex items-center justify-center text-[10px] text-muted-foreground", children: effect.kind === "motion" ? "Motion" : "No preview" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("canvas", { ref, width, height, className: "h-full w-full", "aria-hidden": "true" })
95
+ }
96
+ );
97
+ }
98
+
99
+ // src/effects/EffectsBrowser.tsx
100
+ var import_react3 = require("react");
101
+ var import_effects2 = require("@pixerate/editor/effects");
102
+ var import_jsx_runtime2 = require("react/jsx-runtime");
103
+ function EffectsBrowser({ provider, uses, onPick, thumbnails = true, title = "Effects", className = "" }) {
104
+ const [all, setAll] = (0, import_react3.useState)([]);
105
+ const [query, setQuery] = (0, import_react3.useState)("");
106
+ const [category, setCategory] = (0, import_react3.useState)("");
107
+ const [use, setUse] = (0, import_react3.useState)("");
108
+ const [error, setError] = (0, import_react3.useState)(null);
109
+ (0, import_react3.useEffect)(() => {
110
+ if (!provider) return setAll([]);
111
+ let live = true;
112
+ Promise.resolve().then(() => provider.list()).then(
113
+ (list) => live && (setAll(list), setError(null)),
114
+ (e) => live && setError(e instanceof Error ? e.message : String(e))
115
+ );
116
+ return () => {
117
+ live = false;
118
+ };
119
+ }, [provider]);
120
+ const [found, setFound] = (0, import_react3.useState)([]);
121
+ (0, import_react3.useEffect)(() => {
122
+ if (!provider) return setFound([]);
123
+ let live = true;
124
+ Promise.resolve().then(() => provider.search(query, { ...category ? { category } : {}, ...use ? { use } : {} })).then((list) => live && setFound(list), () => live && setFound([]));
125
+ return () => {
126
+ live = false;
127
+ };
128
+ }, [provider, query, category, use]);
129
+ const allowed = (e) => (uses ? e.kind !== "motion" || uses.includes("motion") : e.kind !== "motion") && (!uses || e.uses.some((u) => uses.includes(u)));
130
+ const categories = (0, import_react3.useMemo)(() => [...new Set(all.filter(allowed).map((e) => e.category))], [all, uses]);
131
+ const shown = found.filter(allowed);
132
+ if (!provider)
133
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `rounded-md border border-dashed border-border p-3 text-xs text-muted-foreground ${className}`, "data-effects-browser": "", "data-effects-unavailable": "", children: "No effects provider is registered, so effects cannot be added. Effects already in the document are kept." });
134
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: `flex flex-col gap-2 ${className}`, "data-effects-browser": "", "aria-label": "Effects browser", children: [
135
+ title && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h3", { className: "text-xs font-semibold", children: title }),
136
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
137
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
138
+ "input",
139
+ {
140
+ type: "search",
141
+ className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1.5 py-0.5 text-xs",
142
+ placeholder: "Search effects",
143
+ "aria-label": "Search effects",
144
+ value: query,
145
+ onChange: (e) => setQuery(e.currentTarget.value)
146
+ }
147
+ ),
148
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", "aria-label": "Category", value: category, onChange: (e) => setCategory(e.currentTarget.value), children: [
149
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "", children: "All categories" }),
150
+ categories.map((c) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: c, children: c }, c))
151
+ ] }),
152
+ uses && uses.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("select", { className: "rounded border border-border bg-transparent px-1 py-0.5 text-xs", "aria-label": "Use", value: use, onChange: (e) => setUse(e.currentTarget.value), children: [
153
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "", children: "Any use" }),
154
+ uses.map((u) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: u, children: import_effects2.EFFECT_USE_LABELS[u] }, u))
155
+ ] })
156
+ ] }),
157
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { role: "alert", className: "text-[11px] text-destructive", children: error }),
158
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { className: "grid grid-cols-[repeat(auto-fill,minmax(6.5rem,1fr))] gap-1.5", role: "list", children: shown.map((e) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
159
+ "button",
160
+ {
161
+ type: "button",
162
+ className: "flex w-full flex-col gap-1 rounded border border-transparent p-1 text-left hover:border-border focus-visible:border-border",
163
+ title: e.description ?? e.name,
164
+ "data-effect-id": e.id,
165
+ onClick: () => onPick(e),
166
+ children: [
167
+ thumbnails && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(EffectThumbnail, { provider, effect: e }),
168
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "truncate text-[11px] font-medium", children: e.name }),
169
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "truncate text-[10px] text-muted-foreground", children: e.uses.map((u) => import_effects2.EFFECT_USE_LABELS[u]).join(" \xB7 ") })
170
+ ]
171
+ }
172
+ ) }, e.id)) }),
173
+ !shown.length && !error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No effects match." })
174
+ ] });
175
+ }
176
+
177
+ // src/effects/EffectParameters.tsx
178
+ var import_react5 = __toESM(require("react"), 1);
179
+ var import_effects3 = require("@pixerate/editor/effects");
180
+ var import_parameters2 = require("@pixerate/editor/parameters");
181
+
182
+ // src/parameters/fields.tsx
183
+ var import_react4 = require("react");
184
+ var import_parameters = require("@pixerate/editor/parameters");
185
+ var import_jsx_runtime3 = require("react/jsx-runtime");
186
+ 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";
187
+ 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";
188
+ var rangeClass = "h-4 w-full min-w-0 flex-1 cursor-pointer accent-primary disabled:cursor-not-allowed disabled:opacity-50";
189
+ function ParameterCommitInput({ value, onCommit, multiline, className, onKeyDown, ...rest }) {
190
+ const commit = (el) => {
191
+ if (el.value !== value) onCommit(el.value, el);
192
+ };
193
+ const keyDown = (e) => {
194
+ onKeyDown?.(e);
195
+ if (e.defaultPrevented) return;
196
+ if (e.key === "Enter" && (!multiline || e.metaKey || e.ctrlKey)) {
197
+ e.preventDefault();
198
+ commit(e.currentTarget);
199
+ }
200
+ if (e.key === "Escape") e.currentTarget.value = value;
201
+ };
202
+ const props = {
203
+ defaultValue: value,
204
+ className: className ?? parameterInputClass,
205
+ onBlur: (e) => commit(e.currentTarget),
206
+ onKeyDown: keyDown,
207
+ ...rest
208
+ };
209
+ return multiline ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("textarea", { ...props, className: `${props.className} h-auto min-h-[3.5rem] py-1` }, value) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { type: "text", ...props }, value);
210
+ }
211
+ var plainNumber = (field, value) => (0, import_parameters.formatParameterNumber)({ ...field, unit: void 0 }, value);
212
+ function ParameterNumberInput({
213
+ field,
214
+ value,
215
+ onChange,
216
+ disabled,
217
+ id,
218
+ "aria-label": ariaLabel
219
+ }) {
220
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "relative min-w-0 flex-1", children: [
221
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
222
+ ParameterCommitInput,
223
+ {
224
+ id,
225
+ inputMode: "decimal",
226
+ "aria-label": ariaLabel ?? (0, import_parameters.parameterLabel)(field),
227
+ value: plainNumber(field, value),
228
+ disabled: disabled || field.readOnly,
229
+ className: `${parameterInputClass} ${field.unit ? "pr-6" : ""}`,
230
+ onCommit: (text, input) => {
231
+ const n = (0, import_parameters.parseParameterNumberInput)(field, text);
232
+ if (n === null || n === value) input.value = plainNumber(field, value);
233
+ else onChange(n);
234
+ },
235
+ onKeyDown: (e) => {
236
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
237
+ e.preventDefault();
238
+ const step = (0, import_parameters.parameterStep)(field) * (e.shiftKey ? 10 : 1);
239
+ onChange((0, import_parameters.clampParameterNumber)(field, value + (e.key === "ArrowUp" ? step : -step)));
240
+ }
241
+ }
242
+ ),
243
+ field.unit && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-[10px] text-muted-foreground", children: field.unit })
244
+ ] });
245
+ }
246
+ function ParameterSlider(props) {
247
+ const { field, value, onChange, onGestureStart, onGestureEnd, disabled } = props;
248
+ const dragging = (0, import_react4.useRef)(false);
249
+ const end = () => {
250
+ if (!dragging.current) return;
251
+ dragging.current = false;
252
+ onGestureEnd?.();
253
+ };
254
+ if (!(0, import_parameters.isSliderField)(field)) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ParameterNumberInput, { ...props });
255
+ const step = (0, import_parameters.parameterStep)(field);
256
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
257
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
258
+ "input",
259
+ {
260
+ type: "range",
261
+ "aria-label": (0, import_parameters.parameterLabel)(field),
262
+ "aria-valuetext": (0, import_parameters.formatParameterNumber)(field, value),
263
+ min: field.min,
264
+ max: field.max,
265
+ step,
266
+ value,
267
+ disabled: disabled || field.readOnly,
268
+ className: rangeClass,
269
+ onPointerDown: () => {
270
+ if (dragging.current) return;
271
+ dragging.current = true;
272
+ onGestureStart?.();
273
+ },
274
+ onPointerUp: end,
275
+ onPointerCancel: end,
276
+ onBlur: end,
277
+ onChange: (e) => onChange((0, import_parameters.clampParameterNumber)(field, Number(e.target.value)))
278
+ }
279
+ ),
280
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "w-20 shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ParameterNumberInput, { ...props, id: props.id, "aria-label": `${(0, import_parameters.parameterLabel)(field)} value` }) })
281
+ ] });
282
+ }
283
+ function ParameterToggle({ field, value, onChange, disabled, id }) {
284
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
285
+ "button",
286
+ {
287
+ type: "button",
288
+ role: "switch",
289
+ id,
290
+ "aria-checked": value,
291
+ "aria-label": (0, import_parameters.parameterLabel)(field),
292
+ disabled: disabled || field.readOnly,
293
+ onClick: () => onChange(!value),
294
+ 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"}`,
295
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
296
+ "span",
297
+ {
298
+ className: `pointer-events-none block h-4 w-4 rounded-full bg-background shadow transition-transform ${value ? "translate-x-4" : "translate-x-0.5"}`
299
+ }
300
+ )
301
+ }
302
+ );
303
+ }
304
+ function ParameterSelect({ field, value, onChange, disabled, id }) {
305
+ const options = (0, import_parameters.getChoiceOptions)(field);
306
+ const off = disabled || field.readOnly;
307
+ if (field.control === "segmented") {
308
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "radiogroup", "aria-label": (0, import_parameters.parameterLabel)(field), className: "inline-flex flex-wrap rounded-md border border-input bg-muted p-0.5", children: options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
309
+ "button",
310
+ {
311
+ type: "button",
312
+ role: "radio",
313
+ "aria-checked": o.value === value,
314
+ title: o.description,
315
+ disabled: off,
316
+ onClick: () => onChange(o.value),
317
+ 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"}`,
318
+ children: (0, import_parameters.choiceOptionLabel)(o)
319
+ },
320
+ String(o.value)
321
+ )) });
322
+ }
323
+ const index = options.findIndex((o) => o.value === value);
324
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
325
+ "select",
326
+ {
327
+ id,
328
+ "aria-label": (0, import_parameters.parameterLabel)(field),
329
+ value: String(index),
330
+ disabled: off,
331
+ onChange: (e) => {
332
+ const option = options[Number(e.target.value)];
333
+ if (option) onChange(option.value);
334
+ },
335
+ className: parameterInputClass,
336
+ children: options.map((o, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: String(i), title: o.description, children: (0, import_parameters.choiceOptionLabel)(o) }, String(o.value)))
337
+ }
338
+ );
339
+ }
340
+ function ParameterColor({ field, value, onChange, onGestureStart, onGestureEnd, disabled, id }) {
341
+ const picking = (0, import_react4.useRef)(false);
342
+ const alpha = !!field.alpha;
343
+ const hex = (0, import_parameters.normalizeHexColor)(value, true) ?? "#000000ff";
344
+ const rgb = hex.slice(0, 7);
345
+ const a = hex.slice(7);
346
+ const off = disabled || field.readOnly;
347
+ const start = () => {
348
+ if (picking.current) return;
349
+ picking.current = true;
350
+ onGestureStart?.();
351
+ };
352
+ const end = () => {
353
+ if (!picking.current) return;
354
+ picking.current = false;
355
+ onGestureEnd?.();
356
+ };
357
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-1.5", children: [
358
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
359
+ /* @__PURE__ */ (0, import_jsx_runtime3.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: [
360
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "absolute inset-0", style: { background: alpha ? hex : rgb } }),
361
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
362
+ "input",
363
+ {
364
+ type: "color",
365
+ "aria-label": (0, import_parameters.parameterLabel)(field),
366
+ value: rgb,
367
+ disabled: off,
368
+ className: "absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed",
369
+ onChange: (e) => {
370
+ start();
371
+ onChange(alpha ? `${e.target.value}${a}` : e.target.value);
372
+ },
373
+ onBlur: end
374
+ }
375
+ )
376
+ ] }),
377
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
378
+ ParameterCommitInput,
379
+ {
380
+ id,
381
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} hex`,
382
+ value: alpha ? hex : rgb,
383
+ disabled: off,
384
+ spellCheck: false,
385
+ className: `${parameterInputClass} font-mono`,
386
+ onCommit: (text) => {
387
+ const next = (0, import_parameters.normalizeHexColor)(text, alpha);
388
+ if (next) onChange(next);
389
+ }
390
+ }
391
+ )
392
+ ] }),
393
+ alpha && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
394
+ "input",
395
+ {
396
+ type: "range",
397
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} opacity`,
398
+ min: 0,
399
+ max: 255,
400
+ step: 1,
401
+ value: parseInt(a, 16),
402
+ disabled: off,
403
+ className: rangeClass,
404
+ onPointerDown: start,
405
+ onPointerUp: end,
406
+ onPointerCancel: end,
407
+ onBlur: end,
408
+ onChange: (e) => onChange(`${rgb}${Number(e.target.value).toString(16).padStart(2, "0")}`)
409
+ }
410
+ ),
411
+ field.swatches && field.swatches.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex flex-wrap gap-1", children: field.swatches.map((s) => {
412
+ const swatch = (0, import_parameters.normalizeHexColor)(s, alpha);
413
+ if (!swatch) return null;
414
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
415
+ "button",
416
+ {
417
+ type: "button",
418
+ title: swatch,
419
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} ${swatch}`,
420
+ "aria-pressed": swatch === (alpha ? hex : rgb),
421
+ disabled: off,
422
+ onClick: () => onChange(swatch),
423
+ className: "h-5 w-5 rounded border border-input aria-pressed:ring-1 aria-pressed:ring-primary",
424
+ style: { background: swatch }
425
+ },
426
+ s
427
+ );
428
+ }) })
429
+ ] });
430
+ }
431
+ function ParameterVector({ field, value, onChange, disabled, id }) {
432
+ const size = (0, import_parameters.vectorSize)(field);
433
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "grid gap-1.5", style: { gridTemplateColumns: `repeat(${size}, minmax(0, 1fr))` }, children: Array.from({ length: size }, (_, i) => {
434
+ const component = (0, import_parameters.vectorComponentField)(field, i);
435
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "flex min-w-0 items-center gap-1", children: [
436
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "w-3 shrink-0 text-[10px] uppercase text-muted-foreground", children: component.label }),
437
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
438
+ ParameterNumberInput,
439
+ {
440
+ field: component,
441
+ value: value[i] ?? 0,
442
+ id: i === 0 ? id : void 0,
443
+ "aria-label": `${(0, import_parameters.parameterLabel)(field)} ${component.label}`,
444
+ disabled: disabled || field.readOnly,
445
+ onChange: (n) => onChange(Array.from({ length: size }, (_2, j) => j === i ? n : value[j] ?? 0))
446
+ }
447
+ )
448
+ ] }, i);
449
+ }) });
450
+ }
451
+ function ParameterImage({ field, value, onChange, disabled, id, resolveFile }) {
452
+ const off = disabled || field.readOnly;
453
+ const name = (0, import_parameters.parameterLabel)(field);
454
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "space-y-1.5", children: [
455
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
456
+ /* @__PURE__ */ (0, import_jsx_runtime3.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__ */ (0, import_jsx_runtime3.jsx)("img", { src: value, alt: name, className: "h-full w-full object-cover" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "None" }) }),
457
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
458
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
459
+ ParameterCommitInput,
460
+ {
461
+ id,
462
+ "aria-label": `${name} URL`,
463
+ placeholder: "Image URL",
464
+ value: value.startsWith("data:") ? "" : value,
465
+ disabled: off,
466
+ onCommit: (text) => onChange(text.trim())
467
+ }
468
+ ),
469
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex gap-1", children: [
470
+ resolveFile && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: `${parameterButtonClass} cursor-pointer ${off ? "pointer-events-none opacity-50" : ""}`, children: [
471
+ "Choose file\u2026",
472
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
473
+ "input",
474
+ {
475
+ type: "file",
476
+ className: "sr-only",
477
+ accept: field.accept ?? "image/*",
478
+ disabled: off,
479
+ "aria-label": `${name} file`,
480
+ onChange: async (e) => {
481
+ const file = e.target.files?.[0];
482
+ e.target.value = "";
483
+ if (file) onChange(await resolveFile(file));
484
+ }
485
+ }
486
+ )
487
+ ] }),
488
+ value && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: parameterButtonClass, disabled: off, onClick: () => onChange(""), "aria-label": `Clear ${name}`, children: "Clear" })
489
+ ] })
490
+ ] })
491
+ ] }),
492
+ field.options && field.options.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex flex-wrap gap-1", children: field.options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
493
+ "button",
494
+ {
495
+ type: "button",
496
+ title: o.label ?? o.url,
497
+ "aria-label": o.label ?? o.url,
498
+ "aria-pressed": o.url === value,
499
+ disabled: off,
500
+ onClick: () => onChange(o.url),
501
+ className: "h-9 w-12 overflow-hidden rounded border border-input aria-pressed:ring-2 aria-pressed:ring-primary",
502
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src: o.url, alt: "", className: "h-full w-full object-cover" })
503
+ },
504
+ o.url
505
+ )) })
506
+ ] });
507
+ }
508
+ function ParameterText({ field, value, onChange, disabled, id }) {
509
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
510
+ ParameterCommitInput,
511
+ {
512
+ id,
513
+ "aria-label": (0, import_parameters.parameterLabel)(field),
514
+ multiline: field.multiline,
515
+ placeholder: field.placeholder,
516
+ maxLength: field.maxLength,
517
+ value,
518
+ disabled: disabled || field.readOnly,
519
+ onCommit: onChange
520
+ }
521
+ );
522
+ }
523
+ function ParameterFieldControl({ field, value, onChange, resolveFile, ...rest }) {
524
+ const set = onChange;
525
+ switch (field.type) {
526
+ case "number":
527
+ case "int":
528
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ParameterSlider, { field, value, onChange: set, ...rest });
529
+ case "boolean":
530
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ParameterToggle, { field, value, onChange: set, ...rest });
531
+ case "choice":
532
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ParameterSelect, { field, value, onChange: set, ...rest });
533
+ case "color":
534
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ParameterColor, { field, value, onChange: set, ...rest });
535
+ case "vector":
536
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ParameterVector, { field, value, onChange: set, ...rest });
537
+ case "image":
538
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ParameterImage, { field, value, onChange: set, resolveFile, ...rest });
539
+ case "text":
540
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ParameterText, { field, value, onChange: set, ...rest });
541
+ }
542
+ }
543
+
544
+ // src/effects/EffectParameters.tsx
545
+ var import_jsx_runtime4 = require("react/jsx-runtime");
546
+ function EffectParameters({ description, values, onChange, onReset, keyframed, onPreset, disabled = false, className = "" }) {
547
+ const fields = (0, import_react5.useMemo)(() => (0, import_effects3.effectParameterFields)(description), [description]);
548
+ const idBase = (0, import_react5.useId)();
549
+ const gesture = import_react5.default.useRef(null);
550
+ const count = import_react5.default.useRef(0);
551
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `space-y-2 ${className}`, "data-effect-parameters": description.id, children: [
552
+ onPreset && description.presets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
553
+ "select",
554
+ {
555
+ className: "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs",
556
+ "aria-label": `${description.name} presets`,
557
+ value: "",
558
+ disabled,
559
+ onChange: (e) => {
560
+ const preset = description.presets.find((p) => p.name === e.currentTarget.value);
561
+ if (preset) onPreset({ ...description.defaults, ...preset.values });
562
+ },
563
+ children: [
564
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "", children: "Presets\u2026" }),
565
+ description.presets.map((p) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: p.name, children: p.name }, p.name))
566
+ ]
567
+ }
568
+ ),
569
+ fields.map((field) => {
570
+ const label = (0, import_parameters2.parameterLabel)(field);
571
+ const inputId = `${idBase}-${field.id}`;
572
+ const keyed = keyframed?.has(field.id) ?? false;
573
+ const value = (0, import_effects3.effectFieldValue)(field, values[field.id]);
574
+ const changed = !keyed && !(0, import_parameters2.parameterValuesEqual)(value, field.default);
575
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "space-y-1", "data-effect-control": field.id, children: [
576
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-1.5", children: [
577
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { htmlFor: inputId, className: "min-w-0 flex-1 truncate text-xs font-medium", title: field.description, children: label }),
578
+ keyed && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-[10px] text-muted-foreground", children: "keyframed" }),
579
+ onReset && changed && !disabled && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: "rounded px-1 text-[11px] text-muted-foreground hover:text-foreground", "aria-label": `Reset ${label}`, onClick: () => onReset(field.id), children: "\u21BA" })
580
+ ] }),
581
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
582
+ ParameterFieldControl,
583
+ {
584
+ field,
585
+ value,
586
+ id: inputId,
587
+ disabled: disabled || keyed,
588
+ onChange: (v) => onChange(field.id, v, gesture.current),
589
+ onGestureStart: () => gesture.current = `${idBase}:${field.id}:${++count.current}`,
590
+ onGestureEnd: () => gesture.current = null
591
+ }
592
+ )
593
+ ] }, field.id);
594
+ }),
595
+ !fields.length && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-[11px] text-muted-foreground", children: "No controls." })
596
+ ] });
597
+ }
598
+ // Annotate the CommonJS export names for ESM import in node:
599
+ 0 && (module.exports = {
600
+ EffectParameters,
601
+ EffectThumbnail,
602
+ EffectsBrowser,
603
+ useEffectDescription,
604
+ useEffects
605
+ });
606
+ //# sourceMappingURL=index.cjs.map