@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,206 @@
1
+ import {
2
+ ParameterFieldControl
3
+ } from "./chunk-ZQZAKUUM.js";
4
+
5
+ // src/effects/useEffects.ts
6
+ import { useEffect, useState } from "react";
7
+ function useEffects(host, provider) {
8
+ useEffect(() => {
9
+ if (!host || !provider) return;
10
+ return host.useEffects(provider);
11
+ }, [host, provider]);
12
+ }
13
+ function useEffectDescription(provider, effect) {
14
+ const [state, setState] = useState({ key: "", description: null, error: null });
15
+ const key = provider && effect ? `${provider.id}:${effect}` : "";
16
+ useEffect(() => {
17
+ if (!provider || !effect) return;
18
+ let live = true;
19
+ Promise.resolve().then(() => provider.describe(effect)).then(
20
+ (description) => live && setState({ key, description, error: null }),
21
+ (e) => live && setState({ key, description: null, error: e instanceof Error ? e.message : String(e) })
22
+ );
23
+ return () => {
24
+ live = false;
25
+ };
26
+ }, [provider, effect, key]);
27
+ return state.key === key ? { description: state.description, error: state.error } : { description: null, error: null };
28
+ }
29
+
30
+ // src/effects/EffectThumbnail.tsx
31
+ import { useEffect as useEffect2, useRef, useState as useState2 } from "react";
32
+ import { renderEffectThumbnail } from "@pixerate/editor/effects";
33
+ import { jsx } from "react/jsx-runtime";
34
+ function EffectThumbnail({ provider, effect, width = 96, height = 54, className = "" }) {
35
+ const ref = useRef(null);
36
+ const [failed, setFailed] = useState2(effect.kind === "motion");
37
+ useEffect2(() => {
38
+ if (effect.kind === "motion") return;
39
+ let live = true;
40
+ renderEffectThumbnail(provider, effect, { width, height }).then(
41
+ (pixels) => {
42
+ const ctx = live ? ref.current?.getContext("2d") : null;
43
+ if (!ctx || typeof ImageData === "undefined") return;
44
+ ctx.putImageData(new ImageData(new Uint8ClampedArray(pixels.data), pixels.width, pixels.height), 0, 0);
45
+ },
46
+ () => live && setFailed(true)
47
+ );
48
+ return () => {
49
+ live = false;
50
+ };
51
+ }, [provider, effect, width, height]);
52
+ return /* @__PURE__ */ jsx(
53
+ "div",
54
+ {
55
+ className: `relative overflow-hidden rounded border border-border bg-[repeating-conic-gradient(#8884_0_25%,transparent_0_50%)] bg-[length:12px_12px] ${className}`,
56
+ style: { aspectRatio: `${width} / ${height}` },
57
+ "data-effect-thumbnail": effect.id,
58
+ children: failed ? /* @__PURE__ */ jsx("span", { className: "absolute inset-0 flex items-center justify-center text-[10px] text-muted-foreground", children: effect.kind === "motion" ? "Motion" : "No preview" }) : /* @__PURE__ */ jsx("canvas", { ref, width, height, className: "h-full w-full", "aria-hidden": "true" })
59
+ }
60
+ );
61
+ }
62
+
63
+ // src/effects/EffectsBrowser.tsx
64
+ import { useEffect as useEffect3, useMemo, useState as useState3 } from "react";
65
+ import { EFFECT_USE_LABELS } from "@pixerate/editor/effects";
66
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
67
+ function EffectsBrowser({ provider, uses, onPick, thumbnails = true, title = "Effects", className = "" }) {
68
+ const [all, setAll] = useState3([]);
69
+ const [query, setQuery] = useState3("");
70
+ const [category, setCategory] = useState3("");
71
+ const [use, setUse] = useState3("");
72
+ const [error, setError] = useState3(null);
73
+ useEffect3(() => {
74
+ if (!provider) return setAll([]);
75
+ let live = true;
76
+ Promise.resolve().then(() => provider.list()).then(
77
+ (list) => live && (setAll(list), setError(null)),
78
+ (e) => live && setError(e instanceof Error ? e.message : String(e))
79
+ );
80
+ return () => {
81
+ live = false;
82
+ };
83
+ }, [provider]);
84
+ const [found, setFound] = useState3([]);
85
+ useEffect3(() => {
86
+ if (!provider) return setFound([]);
87
+ let live = true;
88
+ Promise.resolve().then(() => provider.search(query, { ...category ? { category } : {}, ...use ? { use } : {} })).then((list) => live && setFound(list), () => live && setFound([]));
89
+ return () => {
90
+ live = false;
91
+ };
92
+ }, [provider, query, category, use]);
93
+ const allowed = (e) => (uses ? e.kind !== "motion" || uses.includes("motion") : e.kind !== "motion") && (!uses || e.uses.some((u) => uses.includes(u)));
94
+ const categories = useMemo(() => [...new Set(all.filter(allowed).map((e) => e.category))], [all, uses]);
95
+ const shown = found.filter(allowed);
96
+ if (!provider)
97
+ return /* @__PURE__ */ jsx2("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." });
98
+ return /* @__PURE__ */ jsxs("section", { className: `flex flex-col gap-2 ${className}`, "data-effects-browser": "", "aria-label": "Effects browser", children: [
99
+ title && /* @__PURE__ */ jsx2("h3", { className: "text-xs font-semibold", children: title }),
100
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1", children: [
101
+ /* @__PURE__ */ jsx2(
102
+ "input",
103
+ {
104
+ type: "search",
105
+ className: "min-w-0 flex-1 rounded border border-border bg-transparent px-1.5 py-0.5 text-xs",
106
+ placeholder: "Search effects",
107
+ "aria-label": "Search effects",
108
+ value: query,
109
+ onChange: (e) => setQuery(e.currentTarget.value)
110
+ }
111
+ ),
112
+ /* @__PURE__ */ 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: [
113
+ /* @__PURE__ */ jsx2("option", { value: "", children: "All categories" }),
114
+ categories.map((c) => /* @__PURE__ */ jsx2("option", { value: c, children: c }, c))
115
+ ] }),
116
+ uses && uses.length > 1 && /* @__PURE__ */ 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: [
117
+ /* @__PURE__ */ jsx2("option", { value: "", children: "Any use" }),
118
+ uses.map((u) => /* @__PURE__ */ jsx2("option", { value: u, children: EFFECT_USE_LABELS[u] }, u))
119
+ ] })
120
+ ] }),
121
+ error && /* @__PURE__ */ jsx2("p", { role: "alert", className: "text-[11px] text-destructive", children: error }),
122
+ /* @__PURE__ */ jsx2("ul", { className: "grid grid-cols-[repeat(auto-fill,minmax(6.5rem,1fr))] gap-1.5", role: "list", children: shown.map((e) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsxs(
123
+ "button",
124
+ {
125
+ type: "button",
126
+ className: "flex w-full flex-col gap-1 rounded border border-transparent p-1 text-left hover:border-border focus-visible:border-border",
127
+ title: e.description ?? e.name,
128
+ "data-effect-id": e.id,
129
+ onClick: () => onPick(e),
130
+ children: [
131
+ thumbnails && /* @__PURE__ */ jsx2(EffectThumbnail, { provider, effect: e }),
132
+ /* @__PURE__ */ jsx2("span", { className: "truncate text-[11px] font-medium", children: e.name }),
133
+ /* @__PURE__ */ jsx2("span", { className: "truncate text-[10px] text-muted-foreground", children: e.uses.map((u) => EFFECT_USE_LABELS[u]).join(" \xB7 ") })
134
+ ]
135
+ }
136
+ ) }, e.id)) }),
137
+ !shown.length && !error && /* @__PURE__ */ jsx2("p", { className: "text-[11px] text-muted-foreground", children: "No effects match." })
138
+ ] });
139
+ }
140
+
141
+ // src/effects/EffectParameters.tsx
142
+ import React3, { useId, useMemo as useMemo2 } from "react";
143
+ import { effectFieldValue, effectParameterFields } from "@pixerate/editor/effects";
144
+ import { parameterLabel, parameterValuesEqual } from "@pixerate/editor/parameters";
145
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
146
+ function EffectParameters({ description, values, onChange, onReset, keyframed, onPreset, disabled = false, className = "" }) {
147
+ const fields = useMemo2(() => effectParameterFields(description), [description]);
148
+ const idBase = useId();
149
+ const gesture = React3.useRef(null);
150
+ const count = React3.useRef(0);
151
+ return /* @__PURE__ */ jsxs2("div", { className: `space-y-2 ${className}`, "data-effect-parameters": description.id, children: [
152
+ onPreset && description.presets.length > 0 && /* @__PURE__ */ jsxs2(
153
+ "select",
154
+ {
155
+ className: "w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs",
156
+ "aria-label": `${description.name} presets`,
157
+ value: "",
158
+ disabled,
159
+ onChange: (e) => {
160
+ const preset = description.presets.find((p) => p.name === e.currentTarget.value);
161
+ if (preset) onPreset({ ...description.defaults, ...preset.values });
162
+ },
163
+ children: [
164
+ /* @__PURE__ */ jsx3("option", { value: "", children: "Presets\u2026" }),
165
+ description.presets.map((p) => /* @__PURE__ */ jsx3("option", { value: p.name, children: p.name }, p.name))
166
+ ]
167
+ }
168
+ ),
169
+ fields.map((field) => {
170
+ const label = parameterLabel(field);
171
+ const inputId = `${idBase}-${field.id}`;
172
+ const keyed = keyframed?.has(field.id) ?? false;
173
+ const value = effectFieldValue(field, values[field.id]);
174
+ const changed = !keyed && !parameterValuesEqual(value, field.default);
175
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-1", "data-effect-control": field.id, children: [
176
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5", children: [
177
+ /* @__PURE__ */ jsx3("label", { htmlFor: inputId, className: "min-w-0 flex-1 truncate text-xs font-medium", title: field.description, children: label }),
178
+ keyed && /* @__PURE__ */ jsx3("span", { className: "text-[10px] text-muted-foreground", children: "keyframed" }),
179
+ onReset && changed && !disabled && /* @__PURE__ */ jsx3("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" })
180
+ ] }),
181
+ /* @__PURE__ */ jsx3(
182
+ ParameterFieldControl,
183
+ {
184
+ field,
185
+ value,
186
+ id: inputId,
187
+ disabled: disabled || keyed,
188
+ onChange: (v) => onChange(field.id, v, gesture.current),
189
+ onGestureStart: () => gesture.current = `${idBase}:${field.id}:${++count.current}`,
190
+ onGestureEnd: () => gesture.current = null
191
+ }
192
+ )
193
+ ] }, field.id);
194
+ }),
195
+ !fields.length && /* @__PURE__ */ jsx3("p", { className: "text-[11px] text-muted-foreground", children: "No controls." })
196
+ ] });
197
+ }
198
+
199
+ export {
200
+ useEffects,
201
+ useEffectDescription,
202
+ EffectThumbnail,
203
+ EffectsBrowser,
204
+ EffectParameters
205
+ };
206
+ //# sourceMappingURL=chunk-FX3EREF5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/effects/useEffects.ts","../src/effects/EffectThumbnail.tsx","../src/effects/EffectsBrowser.tsx","../src/effects/EffectParameters.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport type { EffectDescription, EffectProvider } from '@pixerate/editor/effects';\n\n/** Anything that takes an effects provider: every editor controller with effects support. */\nexport interface EffectsHost {\n useEffects(provider: EffectProvider): () => void;\n}\n\n/**\n * Registers `provider` with an editor while mounted (or while it is the one passed), and removes it\n * again on unmount. Pass `null` or `undefined` for no provider: the editor then behaves as without\n * effects support.\n */\nexport function useEffects(host: EffectsHost | null | undefined, provider: EffectProvider | null | undefined): void {\n useEffect(() => {\n if (!host || !provider) return;\n return host.useEffects(provider);\n }, [host, provider]);\n}\n\n/** An effect's description from the provider, loaded when `effect` changes (`null` while loading or on error). */\nexport function useEffectDescription(provider: EffectProvider | null | undefined, effect: string | null | undefined): { description: EffectDescription | null; error: string | null } {\n const [state, setState] = useState<{ key: string; description: EffectDescription | null; error: string | null }>({ key: '', description: null, error: null });\n const key = provider && effect ? `${provider.id}:${effect}` : '';\n useEffect(() => {\n if (!provider || !effect) return;\n let live = true;\n Promise.resolve()\n .then(() => provider.describe(effect))\n .then(\n (description) => live && setState({ key, description, error: null }),\n (e: unknown) => live && setState({ key, description: null, error: e instanceof Error ? e.message : String(e) }),\n );\n return () => {\n live = false;\n };\n }, [provider, effect, key]);\n return state.key === key ? { description: state.description, error: state.error } : { description: null, error: null };\n}\n","import React, { useEffect, useRef, useState } from 'react';\nimport { renderEffectThumbnail, type EffectProvider, type EffectSummary } from '@pixerate/editor/effects';\n\nexport interface EffectThumbnailProps {\n provider: EffectProvider;\n effect: EffectSummary;\n /** Pixel size of the preview (default 96 × 54). */\n width?: number;\n height?: number;\n className?: string;\n}\n\n/**\n * A preview of an effect drawn by the provider's own renderer (filters on a sample picture,\n * transitions half way, generators mid-way through). Motion effects and failures show a label.\n */\nexport function EffectThumbnail({ provider, effect, width = 96, height = 54, className = '' }: EffectThumbnailProps) {\n const ref = useRef<HTMLCanvasElement>(null);\n const [failed, setFailed] = useState(effect.kind === 'motion');\n useEffect(() => {\n if (effect.kind === 'motion') return;\n let live = true;\n renderEffectThumbnail(provider, effect, { width, height }).then(\n (pixels) => {\n const ctx = live ? ref.current?.getContext('2d') : null;\n if (!ctx || typeof ImageData === 'undefined') return;\n ctx.putImageData(new ImageData(new Uint8ClampedArray(pixels.data), pixels.width, pixels.height), 0, 0);\n },\n () => live && setFailed(true),\n );\n return () => {\n live = false;\n };\n }, [provider, effect, width, height]);\n return (\n <div\n className={`relative overflow-hidden rounded border border-border bg-[repeating-conic-gradient(#8884_0_25%,transparent_0_50%)] bg-[length:12px_12px] ${className}`}\n style={{ aspectRatio: `${width} / ${height}` }}\n data-effect-thumbnail={effect.id}\n >\n {failed ? (\n <span className=\"absolute inset-0 flex items-center justify-center text-[10px] text-muted-foreground\">{effect.kind === 'motion' ? 'Motion' : 'No preview'}</span>\n ) : (\n <canvas ref={ref} width={width} height={height} className=\"h-full w-full\" aria-hidden=\"true\" />\n )}\n </div>\n );\n}\n","import React, { useEffect, useMemo, useState } from 'react';\nimport { EFFECT_USE_LABELS, type EffectProvider, type EffectSummary, type EffectUse } from '@pixerate/editor/effects';\nimport { EffectThumbnail } from './EffectThumbnail';\n\nexport interface EffectsBrowserProps {\n /** The editor's provider; without one the browser says effects are unavailable. */\n provider: EffectProvider | null | undefined;\n /** Only effects with one of these uses (default: all but motion). */\n uses?: readonly EffectUse[];\n /** Called with the effect the user picks. */\n onPick: (effect: EffectSummary) => void;\n /** Draw previews with the provider's renderer (default true). */\n thumbnails?: boolean;\n /** Heading (default \"Effects\"). */\n title?: React.ReactNode;\n className?: string;\n}\n\n/**\n * Browses a provider's effects: a search box, a category filter (and a use filter when more than\n * one use is allowed), and a grid of effects with previews. Picking one calls `onPick`.\n */\nexport function EffectsBrowser({ provider, uses, onPick, thumbnails = true, title = 'Effects', className = '' }: EffectsBrowserProps) {\n const [all, setAll] = useState<readonly EffectSummary[]>([]);\n const [query, setQuery] = useState('');\n const [category, setCategory] = useState('');\n const [use, setUse] = useState<EffectUse | ''>('');\n const [error, setError] = useState<string | null>(null);\n useEffect(() => {\n if (!provider) return setAll([]);\n let live = true;\n Promise.resolve()\n .then(() => provider.list())\n .then(\n (list) => live && (setAll(list), setError(null)),\n (e: unknown) => live && setError(e instanceof Error ? e.message : String(e)),\n );\n return () => {\n live = false;\n };\n }, [provider]);\n const [found, setFound] = useState<readonly EffectSummary[]>([]);\n useEffect(() => {\n if (!provider) return setFound([]);\n let live = true;\n Promise.resolve()\n .then(() => provider.search(query, { ...(category ? { category } : {}), ...(use ? { use } : {}) }))\n .then((list) => live && setFound(list), () => live && setFound([]));\n return () => {\n live = false;\n };\n }, [provider, query, category, use]);\n const allowed = (e: EffectSummary) => (uses ? e.kind !== 'motion' || uses.includes('motion') : e.kind !== 'motion') && (!uses || e.uses.some((u) => uses.includes(u)));\n const categories = useMemo(() => [...new Set(all.filter(allowed).map((e) => e.category))], [all, uses]);\n const shown = found.filter(allowed);\n if (!provider)\n return (\n <div className={`rounded-md border border-dashed border-border p-3 text-xs text-muted-foreground ${className}`} data-effects-browser=\"\" data-effects-unavailable=\"\">\n No effects provider is registered, so effects cannot be added. Effects already in the document are kept.\n </div>\n );\n return (\n <section className={`flex flex-col gap-2 ${className}`} data-effects-browser=\"\" aria-label=\"Effects browser\">\n {title && <h3 className=\"text-xs font-semibold\">{title}</h3>}\n <div className=\"flex flex-wrap gap-1\">\n <input\n type=\"search\"\n className=\"min-w-0 flex-1 rounded border border-border bg-transparent px-1.5 py-0.5 text-xs\"\n placeholder=\"Search effects\"\n aria-label=\"Search effects\"\n value={query}\n onChange={(e) => setQuery(e.currentTarget.value)}\n />\n <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)}>\n <option value=\"\">All categories</option>\n {categories.map((c) => (\n <option key={c} value={c}>\n {c}\n </option>\n ))}\n </select>\n {uses && uses.length > 1 && (\n <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 as EffectUse | '')}>\n <option value=\"\">Any use</option>\n {uses.map((u) => (\n <option key={u} value={u}>\n {EFFECT_USE_LABELS[u]}\n </option>\n ))}\n </select>\n )}\n </div>\n {error && <p role=\"alert\" className=\"text-[11px] text-destructive\">{error}</p>}\n <ul className=\"grid grid-cols-[repeat(auto-fill,minmax(6.5rem,1fr))] gap-1.5\" role=\"list\">\n {shown.map((e) => (\n <li key={e.id}>\n <button\n type=\"button\"\n className=\"flex w-full flex-col gap-1 rounded border border-transparent p-1 text-left hover:border-border focus-visible:border-border\"\n title={e.description ?? e.name}\n data-effect-id={e.id}\n onClick={() => onPick(e)}\n >\n {thumbnails && <EffectThumbnail provider={provider} effect={e} />}\n <span className=\"truncate text-[11px] font-medium\">{e.name}</span>\n <span className=\"truncate text-[10px] text-muted-foreground\">{e.uses.map((u) => EFFECT_USE_LABELS[u]).join(' · ')}</span>\n </button>\n </li>\n ))}\n </ul>\n {!shown.length && !error && <p className=\"text-[11px] text-muted-foreground\">No effects match.</p>}\n </section>\n );\n}\n","import React, { useId, useMemo } from 'react';\nimport { effectFieldValue, effectParameterFields, type EffectDescription } from '@pixerate/editor/effects';\nimport { parameterLabel, parameterValuesEqual, type ParameterValue } from '@pixerate/editor/parameters';\nimport { ParameterFieldControl } from '../parameters/fields';\n\nexport interface EffectParametersProps {\n /** The effect's description (`describe`). */\n description: EffectDescription;\n /** Its saved values; controls left out show their defaults. */\n values: Readonly<Record<string, unknown>>;\n /**\n * A control changed. While a slider or colour drag is under way `gesture` is a key unique to\n * that drag, so the host can fold its changes into one undo step; otherwise `null`.\n */\n onChange: (control: string, value: ParameterValue, gesture: string | null) => void;\n /** Resets one control to its default. */\n onReset?: (control: string) => void;\n /** Controls that are keyframed: shown read-only with a note (edit them as keyframes). */\n keyframed?: ReadonlySet<string>;\n /** Applies a preset's values. */\n onPreset?: (values: Readonly<Record<string, unknown>>) => void;\n disabled?: boolean;\n className?: string;\n}\n\n/**\n * The parameter panel of an effect, generated from its JSON Schema controls: sliders for bounded\n * numbers, switches, choices, colour pickers, vectors and text, with each control's description,\n * per-control reset and the effect's presets.\n */\nexport function EffectParameters({ description, values, onChange, onReset, keyframed, onPreset, disabled = false, className = '' }: EffectParametersProps) {\n const fields = useMemo(() => effectParameterFields(description), [description]);\n const idBase = useId();\n const gesture = React.useRef<string | null>(null);\n const count = React.useRef(0);\n return (\n <div className={`space-y-2 ${className}`} data-effect-parameters={description.id}>\n {onPreset && description.presets.length > 0 && (\n <select\n className=\"w-full rounded border border-border bg-transparent px-1 py-0.5 text-xs\"\n aria-label={`${description.name} presets`}\n value=\"\"\n disabled={disabled}\n onChange={(e) => {\n const preset = description.presets.find((p) => p.name === e.currentTarget.value);\n if (preset) onPreset({ ...description.defaults, ...preset.values });\n }}\n >\n <option value=\"\">Presets…</option>\n {description.presets.map((p) => (\n <option key={p.name} value={p.name}>\n {p.name}\n </option>\n ))}\n </select>\n )}\n {fields.map((field) => {\n const label = parameterLabel(field);\n const inputId = `${idBase}-${field.id}`;\n const keyed = keyframed?.has(field.id) ?? false;\n const value = effectFieldValue(field, values[field.id]);\n const changed = !keyed && !parameterValuesEqual(value, field.default);\n return (\n <div key={field.id} className=\"space-y-1\" data-effect-control={field.id}>\n <div className=\"flex items-center gap-1.5\">\n <label htmlFor={inputId} className=\"min-w-0 flex-1 truncate text-xs font-medium\" title={field.description}>\n {label}\n </label>\n {keyed && <span className=\"text-[10px] text-muted-foreground\">keyframed</span>}\n {onReset && changed && !disabled && (\n <button type=\"button\" className=\"rounded px-1 text-[11px] text-muted-foreground hover:text-foreground\" aria-label={`Reset ${label}`} onClick={() => onReset(field.id)}>\n ↺\n </button>\n )}\n </div>\n <ParameterFieldControl\n field={field}\n value={value}\n id={inputId}\n disabled={disabled || keyed}\n onChange={(v) => onChange(field.id, v, gesture.current)}\n onGestureStart={() => (gesture.current = `${idBase}:${field.id}:${++count.current}`)}\n onGestureEnd={() => (gesture.current = null)}\n />\n </div>\n );\n })}\n {!fields.length && <p className=\"text-[11px] text-muted-foreground\">No controls.</p>}\n </div>\n );\n}\n"],"mappings":"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","names":["useEffect","useState","useEffect","useState","jsx","useState","useEffect","React","useMemo","jsx","jsxs","useMemo","React"]}
@@ -0,0 +1,416 @@
1
+ // src/parameters/fields.tsx
2
+ import { useRef } from "react";
3
+ import {
4
+ choiceOptionLabel,
5
+ formatParameterNumber,
6
+ getChoiceOptions,
7
+ isSliderField,
8
+ normalizeHexColor,
9
+ parameterLabel,
10
+ parameterStep,
11
+ parseParameterNumberInput,
12
+ clampParameterNumber,
13
+ vectorComponentField,
14
+ vectorSize
15
+ } from "@pixerate/editor/parameters";
16
+ import { jsx, jsxs } from "react/jsx-runtime";
17
+ 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";
18
+ 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";
19
+ var rangeClass = "h-4 w-full min-w-0 flex-1 cursor-pointer accent-primary disabled:cursor-not-allowed disabled:opacity-50";
20
+ function ParameterCommitInput({ value, onCommit, multiline, className, onKeyDown, ...rest }) {
21
+ const commit = (el) => {
22
+ if (el.value !== value) onCommit(el.value, el);
23
+ };
24
+ const keyDown = (e) => {
25
+ onKeyDown?.(e);
26
+ if (e.defaultPrevented) return;
27
+ if (e.key === "Enter" && (!multiline || e.metaKey || e.ctrlKey)) {
28
+ e.preventDefault();
29
+ commit(e.currentTarget);
30
+ }
31
+ if (e.key === "Escape") e.currentTarget.value = value;
32
+ };
33
+ const props = {
34
+ defaultValue: value,
35
+ className: className ?? parameterInputClass,
36
+ onBlur: (e) => commit(e.currentTarget),
37
+ onKeyDown: keyDown,
38
+ ...rest
39
+ };
40
+ 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);
41
+ }
42
+ var plainNumber = (field, value) => formatParameterNumber({ ...field, unit: void 0 }, value);
43
+ function ParameterNumberInput({
44
+ field,
45
+ value,
46
+ onChange,
47
+ disabled,
48
+ id,
49
+ "aria-label": ariaLabel
50
+ }) {
51
+ return /* @__PURE__ */ jsxs("div", { className: "relative min-w-0 flex-1", children: [
52
+ /* @__PURE__ */ jsx(
53
+ ParameterCommitInput,
54
+ {
55
+ id,
56
+ inputMode: "decimal",
57
+ "aria-label": ariaLabel ?? parameterLabel(field),
58
+ value: plainNumber(field, value),
59
+ disabled: disabled || field.readOnly,
60
+ className: `${parameterInputClass} ${field.unit ? "pr-6" : ""}`,
61
+ onCommit: (text, input) => {
62
+ const n = parseParameterNumberInput(field, text);
63
+ if (n === null || n === value) input.value = plainNumber(field, value);
64
+ else onChange(n);
65
+ },
66
+ onKeyDown: (e) => {
67
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
68
+ e.preventDefault();
69
+ const step = parameterStep(field) * (e.shiftKey ? 10 : 1);
70
+ onChange(clampParameterNumber(field, value + (e.key === "ArrowUp" ? step : -step)));
71
+ }
72
+ }
73
+ ),
74
+ 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 })
75
+ ] });
76
+ }
77
+ function ParameterSlider(props) {
78
+ const { field, value, onChange, onGestureStart, onGestureEnd, disabled } = props;
79
+ const dragging = useRef(false);
80
+ const end = () => {
81
+ if (!dragging.current) return;
82
+ dragging.current = false;
83
+ onGestureEnd?.();
84
+ };
85
+ if (!isSliderField(field)) return /* @__PURE__ */ jsx(ParameterNumberInput, { ...props });
86
+ const step = parameterStep(field);
87
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
88
+ /* @__PURE__ */ jsx(
89
+ "input",
90
+ {
91
+ type: "range",
92
+ "aria-label": parameterLabel(field),
93
+ "aria-valuetext": formatParameterNumber(field, value),
94
+ min: field.min,
95
+ max: field.max,
96
+ step,
97
+ value,
98
+ disabled: disabled || field.readOnly,
99
+ className: rangeClass,
100
+ onPointerDown: () => {
101
+ if (dragging.current) return;
102
+ dragging.current = true;
103
+ onGestureStart?.();
104
+ },
105
+ onPointerUp: end,
106
+ onPointerCancel: end,
107
+ onBlur: end,
108
+ onChange: (e) => onChange(clampParameterNumber(field, Number(e.target.value)))
109
+ }
110
+ ),
111
+ /* @__PURE__ */ jsx("div", { className: "w-20 shrink-0", children: /* @__PURE__ */ jsx(ParameterNumberInput, { ...props, id: props.id, "aria-label": `${parameterLabel(field)} value` }) })
112
+ ] });
113
+ }
114
+ function ParameterToggle({ field, value, onChange, disabled, id }) {
115
+ return /* @__PURE__ */ jsx(
116
+ "button",
117
+ {
118
+ type: "button",
119
+ role: "switch",
120
+ id,
121
+ "aria-checked": value,
122
+ "aria-label": parameterLabel(field),
123
+ disabled: disabled || field.readOnly,
124
+ onClick: () => onChange(!value),
125
+ 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"}`,
126
+ children: /* @__PURE__ */ jsx(
127
+ "span",
128
+ {
129
+ className: `pointer-events-none block h-4 w-4 rounded-full bg-background shadow transition-transform ${value ? "translate-x-4" : "translate-x-0.5"}`
130
+ }
131
+ )
132
+ }
133
+ );
134
+ }
135
+ function ParameterSelect({ field, value, onChange, disabled, id }) {
136
+ const options = getChoiceOptions(field);
137
+ const off = disabled || field.readOnly;
138
+ if (field.control === "segmented") {
139
+ 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(
140
+ "button",
141
+ {
142
+ type: "button",
143
+ role: "radio",
144
+ "aria-checked": o.value === value,
145
+ title: o.description,
146
+ disabled: off,
147
+ onClick: () => onChange(o.value),
148
+ 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"}`,
149
+ children: choiceOptionLabel(o)
150
+ },
151
+ String(o.value)
152
+ )) });
153
+ }
154
+ const index = options.findIndex((o) => o.value === value);
155
+ return /* @__PURE__ */ jsx(
156
+ "select",
157
+ {
158
+ id,
159
+ "aria-label": parameterLabel(field),
160
+ value: String(index),
161
+ disabled: off,
162
+ onChange: (e) => {
163
+ const option = options[Number(e.target.value)];
164
+ if (option) onChange(option.value);
165
+ },
166
+ className: parameterInputClass,
167
+ children: options.map((o, i) => /* @__PURE__ */ jsx("option", { value: String(i), title: o.description, children: choiceOptionLabel(o) }, String(o.value)))
168
+ }
169
+ );
170
+ }
171
+ function ParameterColor({ field, value, onChange, onGestureStart, onGestureEnd, disabled, id }) {
172
+ const picking = useRef(false);
173
+ const alpha = !!field.alpha;
174
+ const hex = normalizeHexColor(value, true) ?? "#000000ff";
175
+ const rgb = hex.slice(0, 7);
176
+ const a = hex.slice(7);
177
+ const off = disabled || field.readOnly;
178
+ const start = () => {
179
+ if (picking.current) return;
180
+ picking.current = true;
181
+ onGestureStart?.();
182
+ };
183
+ const end = () => {
184
+ if (!picking.current) return;
185
+ picking.current = false;
186
+ onGestureEnd?.();
187
+ };
188
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
189
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
190
+ /* @__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: [
191
+ /* @__PURE__ */ jsx("span", { className: "absolute inset-0", style: { background: alpha ? hex : rgb } }),
192
+ /* @__PURE__ */ jsx(
193
+ "input",
194
+ {
195
+ type: "color",
196
+ "aria-label": parameterLabel(field),
197
+ value: rgb,
198
+ disabled: off,
199
+ className: "absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed",
200
+ onChange: (e) => {
201
+ start();
202
+ onChange(alpha ? `${e.target.value}${a}` : e.target.value);
203
+ },
204
+ onBlur: end
205
+ }
206
+ )
207
+ ] }),
208
+ /* @__PURE__ */ jsx(
209
+ ParameterCommitInput,
210
+ {
211
+ id,
212
+ "aria-label": `${parameterLabel(field)} hex`,
213
+ value: alpha ? hex : rgb,
214
+ disabled: off,
215
+ spellCheck: false,
216
+ className: `${parameterInputClass} font-mono`,
217
+ onCommit: (text) => {
218
+ const next = normalizeHexColor(text, alpha);
219
+ if (next) onChange(next);
220
+ }
221
+ }
222
+ )
223
+ ] }),
224
+ alpha && /* @__PURE__ */ jsx(
225
+ "input",
226
+ {
227
+ type: "range",
228
+ "aria-label": `${parameterLabel(field)} opacity`,
229
+ min: 0,
230
+ max: 255,
231
+ step: 1,
232
+ value: parseInt(a, 16),
233
+ disabled: off,
234
+ className: rangeClass,
235
+ onPointerDown: start,
236
+ onPointerUp: end,
237
+ onPointerCancel: end,
238
+ onBlur: end,
239
+ onChange: (e) => onChange(`${rgb}${Number(e.target.value).toString(16).padStart(2, "0")}`)
240
+ }
241
+ ),
242
+ field.swatches && field.swatches.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: field.swatches.map((s) => {
243
+ const swatch = normalizeHexColor(s, alpha);
244
+ if (!swatch) return null;
245
+ return /* @__PURE__ */ jsx(
246
+ "button",
247
+ {
248
+ type: "button",
249
+ title: swatch,
250
+ "aria-label": `${parameterLabel(field)} ${swatch}`,
251
+ "aria-pressed": swatch === (alpha ? hex : rgb),
252
+ disabled: off,
253
+ onClick: () => onChange(swatch),
254
+ className: "h-5 w-5 rounded border border-input aria-pressed:ring-1 aria-pressed:ring-primary",
255
+ style: { background: swatch }
256
+ },
257
+ s
258
+ );
259
+ }) })
260
+ ] });
261
+ }
262
+ function ParameterVector({ field, value, onChange, disabled, id }) {
263
+ const size = vectorSize(field);
264
+ return /* @__PURE__ */ jsx("div", { className: "grid gap-1.5", style: { gridTemplateColumns: `repeat(${size}, minmax(0, 1fr))` }, children: Array.from({ length: size }, (_, i) => {
265
+ const component = vectorComponentField(field, i);
266
+ return /* @__PURE__ */ jsxs("label", { className: "flex min-w-0 items-center gap-1", children: [
267
+ /* @__PURE__ */ jsx("span", { className: "w-3 shrink-0 text-[10px] uppercase text-muted-foreground", children: component.label }),
268
+ /* @__PURE__ */ jsx(
269
+ ParameterNumberInput,
270
+ {
271
+ field: component,
272
+ value: value[i] ?? 0,
273
+ id: i === 0 ? id : void 0,
274
+ "aria-label": `${parameterLabel(field)} ${component.label}`,
275
+ disabled: disabled || field.readOnly,
276
+ onChange: (n) => onChange(Array.from({ length: size }, (_2, j) => j === i ? n : value[j] ?? 0))
277
+ }
278
+ )
279
+ ] }, i);
280
+ }) });
281
+ }
282
+ function ParameterImage({ field, value, onChange, disabled, id, resolveFile }) {
283
+ const off = disabled || field.readOnly;
284
+ const name = parameterLabel(field);
285
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
286
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
287
+ /* @__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" }) }),
288
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
289
+ /* @__PURE__ */ jsx(
290
+ ParameterCommitInput,
291
+ {
292
+ id,
293
+ "aria-label": `${name} URL`,
294
+ placeholder: "Image URL",
295
+ value: value.startsWith("data:") ? "" : value,
296
+ disabled: off,
297
+ onCommit: (text) => onChange(text.trim())
298
+ }
299
+ ),
300
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
301
+ resolveFile && /* @__PURE__ */ jsxs("label", { className: `${parameterButtonClass} cursor-pointer ${off ? "pointer-events-none opacity-50" : ""}`, children: [
302
+ "Choose file\u2026",
303
+ /* @__PURE__ */ jsx(
304
+ "input",
305
+ {
306
+ type: "file",
307
+ className: "sr-only",
308
+ accept: field.accept ?? "image/*",
309
+ disabled: off,
310
+ "aria-label": `${name} file`,
311
+ onChange: async (e) => {
312
+ const file = e.target.files?.[0];
313
+ e.target.value = "";
314
+ if (file) onChange(await resolveFile(file));
315
+ }
316
+ }
317
+ )
318
+ ] }),
319
+ value && /* @__PURE__ */ jsx("button", { type: "button", className: parameterButtonClass, disabled: off, onClick: () => onChange(""), "aria-label": `Clear ${name}`, children: "Clear" })
320
+ ] })
321
+ ] })
322
+ ] }),
323
+ field.options && field.options.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: field.options.map((o) => /* @__PURE__ */ jsx(
324
+ "button",
325
+ {
326
+ type: "button",
327
+ title: o.label ?? o.url,
328
+ "aria-label": o.label ?? o.url,
329
+ "aria-pressed": o.url === value,
330
+ disabled: off,
331
+ onClick: () => onChange(o.url),
332
+ className: "h-9 w-12 overflow-hidden rounded border border-input aria-pressed:ring-2 aria-pressed:ring-primary",
333
+ children: /* @__PURE__ */ jsx("img", { src: o.url, alt: "", className: "h-full w-full object-cover" })
334
+ },
335
+ o.url
336
+ )) })
337
+ ] });
338
+ }
339
+ function ParameterText({ field, value, onChange, disabled, id }) {
340
+ return /* @__PURE__ */ jsx(
341
+ ParameterCommitInput,
342
+ {
343
+ id,
344
+ "aria-label": parameterLabel(field),
345
+ multiline: field.multiline,
346
+ placeholder: field.placeholder,
347
+ maxLength: field.maxLength,
348
+ value,
349
+ disabled: disabled || field.readOnly,
350
+ onCommit: onChange
351
+ }
352
+ );
353
+ }
354
+ function KeyframeDiamond({ state, onToggle, label, disabled, className = "" }) {
355
+ const keyed = state === "keyed";
356
+ return /* @__PURE__ */ jsx(
357
+ "button",
358
+ {
359
+ type: "button",
360
+ "data-key-state": state,
361
+ "aria-pressed": keyed,
362
+ "aria-label": `${keyed ? "Remove" : "Add"} keyframe for ${label}`,
363
+ title: keyed ? "Remove keyframe here" : "Add keyframe at the playhead",
364
+ disabled,
365
+ onClick: onToggle,
366
+ 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}`,
367
+ children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 10 10", className: "h-2.5 w-2.5", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
368
+ "path",
369
+ {
370
+ d: "M5 0.8 9.2 5 5 9.2 0.8 5Z",
371
+ fill: keyed ? "currentColor" : "none",
372
+ stroke: "currentColor",
373
+ strokeWidth: "1.3",
374
+ opacity: state === "animated" ? 0.7 : 1
375
+ }
376
+ ) })
377
+ }
378
+ );
379
+ }
380
+ function ParameterFieldControl({ field, value, onChange, resolveFile, ...rest }) {
381
+ const set = onChange;
382
+ switch (field.type) {
383
+ case "number":
384
+ case "int":
385
+ return /* @__PURE__ */ jsx(ParameterSlider, { field, value, onChange: set, ...rest });
386
+ case "boolean":
387
+ return /* @__PURE__ */ jsx(ParameterToggle, { field, value, onChange: set, ...rest });
388
+ case "choice":
389
+ return /* @__PURE__ */ jsx(ParameterSelect, { field, value, onChange: set, ...rest });
390
+ case "color":
391
+ return /* @__PURE__ */ jsx(ParameterColor, { field, value, onChange: set, ...rest });
392
+ case "vector":
393
+ return /* @__PURE__ */ jsx(ParameterVector, { field, value, onChange: set, ...rest });
394
+ case "image":
395
+ return /* @__PURE__ */ jsx(ParameterImage, { field, value, onChange: set, resolveFile, ...rest });
396
+ case "text":
397
+ return /* @__PURE__ */ jsx(ParameterText, { field, value, onChange: set, ...rest });
398
+ }
399
+ }
400
+
401
+ export {
402
+ parameterInputClass,
403
+ parameterButtonClass,
404
+ ParameterCommitInput,
405
+ ParameterNumberInput,
406
+ ParameterSlider,
407
+ ParameterToggle,
408
+ ParameterSelect,
409
+ ParameterColor,
410
+ ParameterVector,
411
+ ParameterImage,
412
+ ParameterText,
413
+ KeyframeDiamond,
414
+ ParameterFieldControl
415
+ };
416
+ //# sourceMappingURL=chunk-ZQZAKUUM.js.map