@pixerate/editor-svelte 0.172.0 → 0.174.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.
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { indexMotionNodes, type AnimationEditorController, type MotionEffectResult } from '@pixerate/editor/animation-editor';
3
3
  import EffectsBrowser from '../effects/EffectsBrowser.svelte';
4
+ import EffectTemplates from '../effects/EffectTemplates.svelte';
4
5
  import AnimationEffectControls from './AnimationEffectControls.svelte';
5
6
  import { animationEditorStateOf } from './createAnimationEditor.svelte.js';
6
7
 
@@ -50,6 +51,7 @@
50
51
  {#if browse === 'motion'}
51
52
  <EffectsBrowser {provider} uses={['motion']} kinds={['motion']} title="Motion for {target}" onpick={(e) => ((browse = null), report(controller.applyMotionEffect(e.id, [target])))} />
52
53
  {/if}
54
+ <EffectTemplates {provider} disabled={ed.readOnly} fillLabel="New scene from template" onfill={(t, v) => controller.fillTemplate(t, v).then((r) => (r.ok && r.notes.length && (message = r.notes.join(' ')), r))} />
53
55
  {#if browse === 'vector'}
54
56
  <EffectsBrowser {provider} kinds={['vector']} title="Vector effects" onpick={(e) => ((browse = null), report(controller.addVectorEffect(e.id)))} />
55
57
  {/if}
@@ -5,7 +5,8 @@ export type CreateCanvasDocumentOptions<TNode extends CanvasNode = CanvasNode, T
5
5
  * Svelte 5 rune: the save / load / undo contract (kind `canvas`) for nodes and edges held in
6
6
  * `$state`, for `createEditorAutosave` or `createDocumentToolProvider`. Reads `getNodes()` /
7
7
  * `getEdges()` reactively, so changes the history does not see (a finished drag, an edge
8
- * removed by Svelte Flow) count too; selection, measuring and drag frames do not. Parity with
8
+ * removed by Svelte Flow) count too; selection, measuring, drag frames, `omitNodeKeys` /
9
+ * `omitEdgeKeys` and the viewport do not. Parity with
9
10
  * React's `useCanvasEditorDocument`. Call `destroy()` when done.
10
11
  */
11
12
  export declare function createCanvasDocument<TNode extends CanvasNode = CanvasNode, TEdge extends CanvasEdgeLike = CanvasEdge>(options: CreateCanvasDocumentOptions<TNode, TEdge>): EditorDocument<CanvasEditorDocument<TNode, TEdge>>;
@@ -1,13 +1,10 @@
1
- import { createCanvasEditorDocument } from '@pixerate/editor/canvas';
2
- const contentKey = (nodes, edges) => JSON.stringify([
3
- nodes.map(({ selected: _s, dragging: _d, measured: _m, resizing: _r, ...rest }) => rest),
4
- edges.map(({ selected: _s, ...rest }) => rest)
5
- ]);
1
+ import { canvasDocumentContentKey, createCanvasEditorDocument } from '@pixerate/editor/canvas';
6
2
  /**
7
3
  * Svelte 5 rune: the save / load / undo contract (kind `canvas`) for nodes and edges held in
8
4
  * `$state`, for `createEditorAutosave` or `createDocumentToolProvider`. Reads `getNodes()` /
9
5
  * `getEdges()` reactively, so changes the history does not see (a finished drag, an edge
10
- * removed by Svelte Flow) count too; selection, measuring and drag frames do not. Parity with
6
+ * removed by Svelte Flow) count too; selection, measuring, drag frames, `omitNodeKeys` /
7
+ * `omitEdgeKeys` and the viewport do not. Parity with
11
8
  * React's `useCanvasEditorDocument`. Call `destroy()` when done.
12
9
  */
13
10
  export function createCanvasDocument(options) {
@@ -26,7 +23,7 @@ export function createCanvasDocument(options) {
26
23
  const edges = options.getEdges();
27
24
  if (nodes.some((n) => n.dragging))
28
25
  return;
29
- const key = contentKey(nodes, edges);
26
+ const key = canvasDocumentContentKey(nodes, edges, options);
30
27
  if (lastKey !== null && key !== lastKey)
31
28
  listeners.forEach((listener) => listener());
32
29
  lastKey = key;
@@ -0,0 +1,119 @@
1
+ <script lang="ts">
2
+ import { checkTemplateValues, isSubjectTemplateSlot, type EffectProvider, type EffectTemplateDescription, type EffectTemplateSummary } from '@pixerate/editor/effects';
3
+
4
+ interface Props {
5
+ /** The effects provider; nothing shows unless it has templates. */
6
+ provider: EffectProvider | null | undefined;
7
+ /** Fills the chosen template (the editor's `fillTemplate`). Resolves `{ ok: false, error }` to show the refusal; on success the form closes. */
8
+ onfill: (template: string, values: Record<string, unknown>) => Promise<{ ok: true } | { ok: false; error: string }>;
9
+ /** The button that fills (default "Use template"). */
10
+ fillLabel?: string;
11
+ disabled?: boolean;
12
+ class?: string;
13
+ }
14
+
15
+ let { provider, onfill, fillLabel = 'Use template', disabled = false, class: className = '' }: Props = $props();
16
+ const input = 'w-full min-w-0 rounded border border-border bg-transparent px-1 py-0.5 text-xs';
17
+ const btn = 'rounded border border-border px-2 py-0.5 text-[11px] disabled:opacity-40';
18
+
19
+ const supported = $derived(!!provider?.capabilities.supportsTemplates && !!provider.listTemplates && !!provider.describeTemplate);
20
+ let templates = $state<readonly EffectTemplateSummary[]>([]);
21
+ let chosen = $state('');
22
+ let description = $state<EffectTemplateDescription | null>(null);
23
+ let values = $state<Record<string, unknown>>({});
24
+ let message = $state<string | null>(null);
25
+ let busy = $state(false);
26
+
27
+ $effect(() => {
28
+ const p = provider;
29
+ const ok = supported;
30
+ let live = true;
31
+ templates = [];
32
+ chosen = '';
33
+ if (ok) void Promise.resolve(p!.listTemplates!()).then((t) => live && (templates = t), () => live && (templates = []));
34
+ return () => (live = false);
35
+ });
36
+
37
+ $effect(() => {
38
+ const id = chosen;
39
+ const p = provider;
40
+ let live = true;
41
+ description = null;
42
+ message = null;
43
+ if (!id || !supported) return;
44
+ void Promise.resolve(p!.describeTemplate!(id)).then(
45
+ (d) => {
46
+ if (!live) return;
47
+ description = d;
48
+ values = Object.fromEntries(d.slots.map((s) => [s.name, isSubjectTemplateSlot(s) ? '' : (s as { default: unknown }).default]));
49
+ },
50
+ (e: Error) => live && (message = e.message)
51
+ );
52
+ return () => (live = false);
53
+ });
54
+
55
+ // Empty subject fields are left out (their default, or left open); empty numbers too.
56
+ const given = $derived(Object.fromEntries(Object.entries(values).filter(([, v]) => v !== '')));
57
+ const problems = $derived(description ? checkTemplateValues(description.slots, given) : []);
58
+ const problemFor = (slot: string) => problems.find((p) => p.startsWith(`Slot "${slot}"`));
59
+
60
+ async function fill(e: SubmitEvent) {
61
+ e.preventDefault();
62
+ if (!description || problems.length) return;
63
+ busy = true;
64
+ const r = await onfill(description.id, given);
65
+ busy = false;
66
+ if (r.ok) chosen = '';
67
+ else message = r.error;
68
+ }
69
+ </script>
70
+
71
+ <!--
72
+ The provider's sequence templates: pick one, fill its slots in a form (built from
73
+ describeTemplate, checked with checkTemplateValues as you type) and use it. Hidden when the
74
+ provider has no templates.
75
+ -->
76
+ {#if supported && templates.length}
77
+ <div class="flex flex-col gap-1 text-xs {className}" data-effect-templates="">
78
+ <select class={input} aria-label="Template" {disabled} bind:value={chosen}>
79
+ <option value="">Templates…</option>
80
+ {#each templates as t (t.id)}<option value={t.id}>{t.name} ({t.shots} shots)</option>{/each}
81
+ </select>
82
+ {#if description}
83
+ <form class="flex flex-col gap-1 rounded border border-border p-1" data-template-form={description.id} onsubmit={fill}>
84
+ <p class="text-[11px] text-muted-foreground">{description.description}</p>
85
+ {#each description.slots as slot (slot.name)}
86
+ {@const id = `template-slot-${slot.name}`}
87
+ {@const error = problemFor(slot.name)}
88
+ <div class="flex flex-col gap-0.5" data-template-slot={slot.name}>
89
+ <label for={id} class="text-muted-foreground" title={slot.description}>{slot.label}</label>
90
+ {#if slot.type === 'color'}
91
+ <input {id} class="h-6 w-10" type="color" value={String(values[slot.name])} oninput={(e) => (values = { ...values, [slot.name]: e.currentTarget.value })} />
92
+ {:else if slot.type === 'number'}
93
+ <input {id} class={input} type="number" min={slot.min} max={slot.max} step={slot.integer ? 1 : 'any'} value={String(values[slot.name])} oninput={(e) => (values = { ...values, [slot.name]: e.currentTarget.value === '' ? '' : Number(e.currentTarget.value) })} />
94
+ {:else if slot.type === 'text'}
95
+ <div class="flex items-center gap-1">
96
+ <input {id} class={input} maxlength={slot.maxLength} value={String(values[slot.name])} oninput={(e) => (values = { ...values, [slot.name]: e.currentTarget.value })} />
97
+ {#if slot.maxLength !== undefined}<span class="text-[10px] text-muted-foreground">{String(values[slot.name]).length}/{slot.maxLength}</span>{/if}
98
+ </div>
99
+ {:else}
100
+ <input
101
+ {id}
102
+ class={input}
103
+ placeholder={slot.hasDefault ? 'URL or <svg> (default: a placeholder)' : 'URL or <svg> (empty: left open)'}
104
+ value={typeof values[slot.name] === 'string' ? (values[slot.name] as string) : ''}
105
+ oninput={(e) => (values = { ...values, [slot.name]: e.currentTarget.value })}
106
+ />
107
+ {/if}
108
+ {#if error}<p class="text-[11px] text-destructive">{error}</p>{/if}
109
+ </div>
110
+ {/each}
111
+ <div class="flex gap-1">
112
+ <button type="submit" class={btn} disabled={disabled || busy || problems.length > 0}>{fillLabel}</button>
113
+ <button type="button" class={btn} onclick={() => (chosen = '')}>Cancel</button>
114
+ </div>
115
+ </form>
116
+ {/if}
117
+ {#if message}<p role="alert" class="text-[11px] text-destructive">{message}</p>{/if}
118
+ </div>
119
+ {/if}
@@ -0,0 +1,19 @@
1
+ import { type EffectProvider } from '@pixerate/editor/effects';
2
+ interface Props {
3
+ /** The effects provider; nothing shows unless it has templates. */
4
+ provider: EffectProvider | null | undefined;
5
+ /** Fills the chosen template (the editor's `fillTemplate`). Resolves `{ ok: false, error }` to show the refusal; on success the form closes. */
6
+ onfill: (template: string, values: Record<string, unknown>) => Promise<{
7
+ ok: true;
8
+ } | {
9
+ ok: false;
10
+ error: string;
11
+ }>;
12
+ /** The button that fills (default "Use template"). */
13
+ fillLabel?: string;
14
+ disabled?: boolean;
15
+ class?: string;
16
+ }
17
+ declare const EffectTemplates: import("svelte").Component<Props, {}, "">;
18
+ type EffectTemplates = ReturnType<typeof EffectTemplates>;
19
+ export default EffectTemplates;
@@ -3,12 +3,15 @@ import type EffectsBrowserComponent from "./EffectsBrowser.svelte";
3
3
  import type EffectParametersComponent from "./EffectParameters.svelte";
4
4
  import type EffectThumbnailComponent from "./EffectThumbnail.svelte";
5
5
  import type EffectAuthoringEditorComponent from "./EffectAuthoringEditor.svelte";
6
+ import type EffectTemplatesComponent from "./EffectTemplates.svelte";
6
7
  export { default as EffectsBrowser } from "./EffectsBrowser.svelte";
7
8
  export { default as EffectParameters } from "./EffectParameters.svelte";
8
9
  export { default as EffectThumbnail } from "./EffectThumbnail.svelte";
9
10
  export { default as EffectAuthoringEditor } from "./EffectAuthoringEditor.svelte";
11
+ export { default as EffectTemplates } from "./EffectTemplates.svelte";
10
12
  export * from "./effects.svelte.js";
11
13
  export type EffectsBrowserProps = ComponentProps<typeof EffectsBrowserComponent>;
12
14
  export type EffectParametersProps = ComponentProps<typeof EffectParametersComponent>;
13
15
  export type EffectThumbnailProps = ComponentProps<typeof EffectThumbnailComponent>;
14
16
  export type EffectAuthoringEditorProps = ComponentProps<typeof EffectAuthoringEditorComponent>;
17
+ export type EffectTemplatesProps = ComponentProps<typeof EffectTemplatesComponent>;
@@ -2,4 +2,5 @@ export { default as EffectsBrowser } from "./EffectsBrowser.svelte";
2
2
  export { default as EffectParameters } from "./EffectParameters.svelte";
3
3
  export { default as EffectThumbnail } from "./EffectThumbnail.svelte";
4
4
  export { default as EffectAuthoringEditor } from "./EffectAuthoringEditor.svelte";
5
+ export { default as EffectTemplates } from "./EffectTemplates.svelte";
5
6
  export * from "./effects.svelte.js";
@@ -2,6 +2,7 @@
2
2
  import { findVideoClip, isVideoKeyframes, openVideoClipEffectAuthoring, type VideoClipEffectAuthoring, type VideoEditorController, type VideoEffectEditResult, type VideoEffectValues } from '@pixerate/editor/video-editor';
3
3
  import { onDestroy } from 'svelte';
4
4
  import EffectsBrowser from '../effects/EffectsBrowser.svelte';
5
+ import EffectTemplates from '../effects/EffectTemplates.svelte';
5
6
  import EffectAuthoringEditor from '../effects/EffectAuthoringEditor.svelte';
6
7
  import EffectControls from './EffectControls.svelte';
7
8
  import { videoEditorStateOf } from './createVideoEditor.svelte.js';
@@ -75,6 +76,7 @@
75
76
  {#if browse === 'generator'}
76
77
  <EffectsBrowser {provider} uses={['generator', 'title', 'overlay']} title="Generators and titles" onpick={(e) => ((browse = null), addEffectClip(e.id))} />
77
78
  {/if}
79
+ <EffectTemplates {provider} {disabled} fillLabel="Insert template" onfill={(t, v) => controller.fillTemplate(t, v, { at: Math.round(ed.time) }).then((r) => (r.ok && r.notes.length && (message = r.notes.join(' ')), r))} />
78
80
  {#if !clip}
79
81
  <p class="text-[11px] text-muted-foreground">Select a clip to see its effects.</p>
80
82
  {:else}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.172.0",
3
+ "version": "0.174.0",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -165,7 +165,7 @@
165
165
  "**/*.css"
166
166
  ],
167
167
  "dependencies": {
168
- "@pixerate/editor": "0.233.0"
168
+ "@pixerate/editor": "0.235.0"
169
169
  },
170
170
  "peerDependencies": {
171
171
  "@dagrejs/dagre": "^1.1.4",