@pixerate/editor-svelte 0.101.4 → 0.102.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,4 +1,4 @@
1
- import type { AgentToolProvider, ToolRegistry } from "@pixerate/editor/tools";
1
+ import type { AgentPlayback, AgentToolProvider, ToolRegistry } from "@pixerate/editor/tools";
2
2
  type MaybeGetter<T> = T | (() => T);
3
3
  /**
4
4
  * Registers a tool provider with a `ToolRegistry` for the lifetime of the
@@ -7,5 +7,7 @@ type MaybeGetter<T> = T | (() => T);
7
7
  * `useAgentToolProvider(registry, () => editor && createRichTextToolProvider(editor, { id }))`:
8
8
  * when the getter returns a different provider, the old one is unregistered.
9
9
  */
10
- export declare function useAgentToolProvider(registry: MaybeGetter<ToolRegistry | null | undefined>, provider: MaybeGetter<AgentToolProvider | null | undefined>): void;
10
+ export declare function useAgentToolProvider(registry: MaybeGetter<ToolRegistry | null | undefined>, provider: MaybeGetter<AgentToolProvider | null | undefined>, options?: {
11
+ playback?: MaybeGetter<AgentPlayback | null | undefined>;
12
+ }): void;
11
13
  export {};
@@ -6,7 +6,14 @@ const read = (value) => typeof value === "function" ? value() : value;
6
6
  * `useAgentToolProvider(registry, () => editor && createRichTextToolProvider(editor, { id }))`:
7
7
  * when the getter returns a different provider, the old one is unregistered.
8
8
  */
9
- export function useAgentToolProvider(registry, provider) {
9
+ export function useAgentToolProvider(registry, provider, options = {}) {
10
+ $effect(() => {
11
+ const currentRegistry = read(registry);
12
+ const playback = options.playback === undefined ? undefined : read(options.playback);
13
+ // Plays the registry's calls with this playback (installed once per registry).
14
+ if (currentRegistry && playback)
15
+ playback.install(currentRegistry);
16
+ });
10
17
  $effect(() => {
11
18
  const currentRegistry = read(registry);
12
19
  const currentProvider = read(provider);
@@ -0,0 +1,30 @@
1
+ import { type CanvasEdge, type CanvasNode, type NodeRectOptions } from '@pixerate/editor/canvas';
2
+ import type { AgentPlayback, PresenceStore } from '@pixerate/editor/tools';
3
+ type MaybeGetter<T> = T | (() => T);
4
+ export interface CreateCanvasPlaybackSurfaceOptions<TNode extends CanvasNode = CanvasNode, TEdge extends CanvasEdge = CanvasEdge> {
5
+ /** The flow's nodes and edges now (e.g. the `bind:nodes` state). */
6
+ getNodes: () => readonly TNode[];
7
+ getEdges?: () => readonly TEdge[];
8
+ /** The flow viewport now (Svelte Flow's `useSvelteFlow().getViewport`). */
9
+ getViewport: () => {
10
+ x: number;
11
+ y: number;
12
+ zoom: number;
13
+ };
14
+ /** The element the flow fills (and the `<PresenceLayer>` sits in); for bounds and screen mapping. */
15
+ container: () => HTMLElement | null | undefined;
16
+ /** Node size resolution (match the canvas' own). */
17
+ rectOptions?: NodeRectOptions;
18
+ /** Bring the canvas on screen (switch to its tab) before the agent acts. */
19
+ reveal?: () => void | Promise<void>;
20
+ /** Whether the canvas is on screen (default: the container has a size and the page is visible). */
21
+ visible?: () => boolean;
22
+ }
23
+ /**
24
+ * Svelte 5 rune: attaches the node canvas as an agent playback surface under `id` (the canvas tool
25
+ * provider's id) for the component's lifetime (call during init) and returns its cursors. Draw them
26
+ * with `<PresenceLayer presence={presence} viewport={viewport} />` inside the container. Parity with
27
+ * React's `useCanvasPlaybackSurface`.
28
+ */
29
+ export declare function createCanvasPlaybackSurface<TNode extends CanvasNode = CanvasNode, TEdge extends CanvasEdge = CanvasEdge>(playback: MaybeGetter<AgentPlayback | null | undefined>, id: MaybeGetter<string | null | undefined>, options: CreateCanvasPlaybackSurfaceOptions<TNode, TEdge>): PresenceStore;
30
+ export {};
@@ -0,0 +1,43 @@
1
+ import { createCanvasPlaybackLocator } from '@pixerate/editor/canvas';
2
+ import { createPlaybackSurface } from '../playback/playback.svelte.js';
3
+ /**
4
+ * Svelte 5 rune: attaches the node canvas as an agent playback surface under `id` (the canvas tool
5
+ * provider's id) for the component's lifetime (call during init) and returns its cursors. Draw them
6
+ * with `<PresenceLayer presence={presence} viewport={viewport} />` inside the container. Parity with
7
+ * React's `useCanvasPlaybackSurface`.
8
+ */
9
+ export function createCanvasPlaybackSurface(playback, id, options) {
10
+ const locate = createCanvasPlaybackLocator({
11
+ getNodes: options.getNodes,
12
+ getEdges: () => options.getEdges?.() ?? [],
13
+ rectOptions: options.rectOptions
14
+ });
15
+ const box = () => options.container()?.getBoundingClientRect() ?? null;
16
+ return createPlaybackSurface(playback, id, {
17
+ defaultKind: 'node',
18
+ locate,
19
+ bounds: () => {
20
+ const r = box();
21
+ const { x, y, zoom } = options.getViewport();
22
+ return r && r.width > 0 ? { x: -x / zoom, y: -y / zoom, width: r.width / zoom, height: r.height / zoom } : null;
23
+ },
24
+ visible: () => {
25
+ if (options.visible)
26
+ return options.visible();
27
+ const r = box();
28
+ const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden';
29
+ return !!r && r.width > 0 && r.height > 0 && !hidden;
30
+ },
31
+ toViewport: (p) => {
32
+ const r = box();
33
+ const { x, y, zoom } = options.getViewport();
34
+ return { x: (r?.left ?? 0) + p.x * zoom + x, y: (r?.top ?? 0) + p.y * zoom + y };
35
+ },
36
+ fromViewport: (p) => {
37
+ const r = box();
38
+ const { x, y, zoom } = options.getViewport();
39
+ return { x: (p.x - (r?.left ?? 0) - x) / zoom, y: (p.y - (r?.top ?? 0) - y) / zoom };
40
+ },
41
+ reveal: () => options.reveal?.()
42
+ });
43
+ }
@@ -18,6 +18,8 @@ export { createStreamingCanvas, type CreateStreamingCanvasOptions } from './crea
18
18
  export { createCanvasArrange, createCanvasArrangeShortcuts, type CanvasArrange, type CreateCanvasArrangeOptions, type CanvasArrangeShortcutsOptions } from './createCanvasArrange.svelte.js';
19
19
  export { createCanvasScenes, type CanvasScenes, type CreateCanvasScenesOptions } from './createCanvasScenes.svelte.js';
20
20
  export { createCanvasCapture, type CreateCanvasCaptureOptions } from './createCanvasCapture.js';
21
+ export { createCanvasPlaybackSurface, type CreateCanvasPlaybackSurfaceOptions } from './createCanvasPlaybackSurface.svelte.js';
22
+ export { default as PresenceLayer } from '../playback/PresenceLayer.svelte';
21
23
  export { default as CanvasGuidesLayer } from './CanvasGuidesLayer.svelte';
22
24
  export { createCanvasDraw, type CanvasDraw, type CreateCanvasDrawOptions } from './createCanvasDraw.svelte.js';
23
25
  export { default as CanvasDrawLayer } from './CanvasDrawLayer.svelte';
@@ -26,7 +26,8 @@ export { createCanvasMultiDrag } from './createCanvasMultiDrag.svelte.js';
26
26
  export { getMarqueeSelectionPreset, handleDeselectOnEscape } from './createCanvasSelection.svelte.js';
27
27
  // Viewport floating scrollbar component
28
28
  export { default as FloatingHorizontalScrollbar } from './FloatingHorizontalScrollbar.svelte';
29
- // Agent presence visual layer
29
+ // Agent presence visual layer. Deprecated: use `PresenceLayer` with `createCanvasPlaybackSurface`
30
+ // (agent playback), which plays every canvas tool call.
30
31
  export { default as AgentPresenceLayer } from './AgentPresenceLayer.svelte';
31
32
  export { default as AgentCursorPresence } from './AgentCursorPresence.svelte';
32
33
  // Streaming canvas rune
@@ -35,6 +36,8 @@ export { createStreamingCanvas } from './createStreamingCanvas.svelte.js';
35
36
  export { createCanvasArrange, createCanvasArrangeShortcuts } from './createCanvasArrange.svelte.js';
36
37
  export { createCanvasScenes } from './createCanvasScenes.svelte.js';
37
38
  export { createCanvasCapture } from './createCanvasCapture.js';
39
+ export { createCanvasPlaybackSurface } from './createCanvasPlaybackSurface.svelte.js';
40
+ export { default as PresenceLayer } from '../playback/PresenceLayer.svelte';
38
41
  export { default as CanvasGuidesLayer } from './CanvasGuidesLayer.svelte';
39
42
  // Freehand drawing tools (pen, highlighter, eraser)
40
43
  export { createCanvasDraw } from './createCanvasDraw.svelte.js';
@@ -11,8 +11,13 @@
11
11
  ImageEditorAIHooks,
12
12
  ImageEditorFit,
13
13
  } from '@pixerate/editor/image-editor';
14
+ import type { AgentPlayback, PlaybackPoint, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
15
+ import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
16
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
14
17
  import {
15
18
  canvasToImagePoint,
19
+ imageToCanvasPoint,
20
+ createImagePlaybackLocator,
16
21
  hitTestCrop,
17
22
  getCropCursor,
18
23
  calculateCropDrag,
@@ -71,6 +76,15 @@
71
76
  * with an Effects section in the Layers tab. Optional: without one the editor is as before.
72
77
  */
73
78
  effects?: EffectProvider | null;
79
+ /**
80
+ * Agent playback: act out the image tools' calls here with the agent's cursor, opening the
81
+ * panel each tool belongs to (default: `setAgentPlayback` above; `null` turns it off).
82
+ */
83
+ playback?: AgentPlayback | null;
84
+ /** The playback surface id (default `image`, where the image tool providers play). */
85
+ playbackId?: string;
86
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
87
+ onplaybackreveal?: () => void | Promise<void>;
74
88
  }
75
89
 
76
90
  let {
@@ -85,6 +99,9 @@
85
99
  aiHooks,
86
100
  fit,
87
101
  effects,
102
+ playback,
103
+ playbackId = 'image',
104
+ onplaybackreveal,
88
105
  }: Props = $props();
89
106
 
90
107
  const internalEditor = createReactiveImageEditor();
@@ -101,6 +118,67 @@
101
118
  let canvasEl = $state<HTMLCanvasElement | null>(null);
102
119
  let containerEl = $state<HTMLDivElement | null>(null);
103
120
  let activeTab = $state<'crop' | 'adjust' | 'layers' | 'paint' | 'select' | 'annotate' | 'depth' | 'ai'>('crop');
121
+ let rootEl = $state<HTMLDivElement | null>(null);
122
+
123
+ // Agent playback: the agent's cursor in document pixels, over the whole editor (canvas and panels).
124
+ const ambientPlayback = getAgentPlayback();
125
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
126
+ /** Document pixels → pixels in the editor's root element, through the canvas projection. */
127
+ const docToRoot = (p: PlaybackPoint): PlaybackPoint => {
128
+ if (!canvasEl || !rootEl || !canvasEl.width) return p;
129
+ const r = canvasEl.getBoundingClientRect();
130
+ const box = rootEl.getBoundingClientRect();
131
+ const c = imageToCanvasPoint(p, editor.controller.getState(), canvasEl.width, canvasEl.height, activeTab === 'crop');
132
+ return { x: r.left - box.left + (c.x * r.width) / canvasEl.width, y: r.top - box.top + (c.y * r.height) / canvasEl.height };
133
+ };
134
+ /** Viewport (client) pixels → document pixels. */
135
+ const clientToDoc = (p: PlaybackPoint): PlaybackPoint => {
136
+ if (!canvasEl || !canvasEl.width) return p;
137
+ const r = canvasEl.getBoundingClientRect();
138
+ const sx = r.width > 0 ? canvasEl.width / r.width : 1;
139
+ const sy = r.height > 0 ? canvasEl.height / r.height : 1;
140
+ return canvasToImagePoint({ x: (p.x - r.left) * sx, y: (p.y - r.top) * sy }, editor.controller.getState(), canvasEl.width, canvasEl.height, activeTab === 'crop');
141
+ };
142
+ /** An element of the editor (a tab, slider or layer row) as a rectangle in document pixels. */
143
+ const elementRect = (selector: string): PlaybackRect | null => {
144
+ const r = rootEl?.querySelector(selector)?.getBoundingClientRect();
145
+ if (!r || r.width <= 0) return null;
146
+ const a = clientToDoc({ x: r.left, y: r.top });
147
+ const b = clientToDoc({ x: r.right, y: r.bottom });
148
+ return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
149
+ };
150
+ const agentPresence = createPlaybackSurface(() => playing, () => playbackId, () => ({
151
+ defaultKind: 'layer',
152
+ locate: (ref: PlaybackRef) => {
153
+ if (ref.kind === 'panel') return elementRect(`[data-image-tab="${CSS.escape(ref.id)}"]`);
154
+ if (ref.kind === 'control') return elementRect(`[data-control-id="${CSS.escape(ref.id)}"]`);
155
+ return createImagePlaybackLocator(editor.controller)(ref) ?? (ref.kind === 'layer' || ref.kind === 'item' ? elementRect(`[data-layer-id="${CSS.escape(ref.id)}"]`) : null);
156
+ },
157
+ open: async (ref: PlaybackRef) => {
158
+ if (ref.kind !== 'panel' || activeTab === ref.id) return;
159
+ rootEl?.querySelector<HTMLElement>(`[data-image-tab="${CSS.escape(ref.id)}"]`)?.click();
160
+ // Let the panel render before the cursor looks for its controls.
161
+ await new Promise((resolve) => setTimeout(resolve, 60));
162
+ },
163
+ bounds: () => {
164
+ const r = canvasEl?.getBoundingClientRect();
165
+ if (!r || r.width <= 0) return null;
166
+ const a = clientToDoc({ x: r.left, y: r.top });
167
+ const b = clientToDoc({ x: r.right, y: r.bottom });
168
+ return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
169
+ },
170
+ visible: () => {
171
+ const r = rootEl?.getBoundingClientRect();
172
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
173
+ },
174
+ toViewport: (p: PlaybackPoint) => {
175
+ const box = rootEl?.getBoundingClientRect();
176
+ const q = docToRoot(p);
177
+ return { x: (box?.left ?? 0) + q.x, y: (box?.top ?? 0) + q.y };
178
+ },
179
+ fromViewport: clientToDoc,
180
+ reveal: () => onplaybackreveal?.()
181
+ }));
104
182
 
105
183
  // Selection tools (magic wand, marquees, lasso): options bar state and the pointer gesture.
106
184
  let selectionOptions = $state<SelectionToolOptions>({ ...DEFAULT_SELECTION_TOOL_OPTIONS });
@@ -883,15 +961,20 @@
883
961
  </script>
884
962
 
885
963
  <div
886
- class="flex flex-col bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-2xl text-slate-100 {className}"
964
+ bind:this={rootEl}
965
+ class="relative flex flex-col bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-2xl text-slate-100 {className}"
887
966
  {style}
888
967
  >
968
+ {#if playing}
969
+ <PresenceLayer presence={agentPresence} viewport={docToRoot} />
970
+ {/if}
889
971
  <!-- Top Action Header -->
890
972
  <div class="flex flex-wrap items-center justify-between px-4 py-3 bg-slate-950/80 border-b border-slate-800 gap-3">
891
973
  <!-- Navigation Tabs -->
892
974
  <div class="flex items-center gap-1 bg-slate-900 p-1 rounded-lg border border-slate-800 text-xs">
893
975
  <button
894
976
  type="button"
977
+ data-image-tab="crop"
895
978
  onclick={() => {
896
979
  activeTab = 'crop';
897
980
  editor.setTool('crop');
@@ -904,6 +987,7 @@
904
987
  </button>
905
988
  <button
906
989
  type="button"
990
+ data-image-tab="adjust"
907
991
  onclick={() => {
908
992
  activeTab = 'adjust';
909
993
  editor.setTool('select');
@@ -916,6 +1000,7 @@
916
1000
  </button>
917
1001
  <button
918
1002
  type="button"
1003
+ data-image-tab="layers"
919
1004
  onclick={() => {
920
1005
  activeTab = 'layers';
921
1006
  editor.setTool('select');
@@ -928,6 +1013,7 @@
928
1013
  </button>
929
1014
  <button
930
1015
  type="button"
1016
+ data-image-tab="paint"
931
1017
  onclick={() => {
932
1018
  activeTab = 'paint';
933
1019
  editor.setTool('brush');
@@ -940,6 +1026,7 @@
940
1026
  </button>
941
1027
  <button
942
1028
  type="button"
1029
+ data-image-tab="select"
943
1030
  onclick={() => {
944
1031
  activeTab = 'select';
945
1032
  editor.setTool('select');
@@ -952,6 +1039,7 @@
952
1039
  </button>
953
1040
  <button
954
1041
  type="button"
1042
+ data-image-tab="annotate"
955
1043
  onclick={() => {
956
1044
  activeTab = 'annotate';
957
1045
  editor.setTool('pen');
@@ -964,6 +1052,7 @@
964
1052
  </button>
965
1053
  <button
966
1054
  type="button"
1055
+ data-image-tab="depth"
967
1056
  onclick={() => {
968
1057
  activeTab = 'depth';
969
1058
  editor.setTool('select');
@@ -976,6 +1065,7 @@
976
1065
  </button>
977
1066
  <button
978
1067
  type="button"
1068
+ data-image-tab="ai"
979
1069
  onclick={() => {
980
1070
  activeTab = 'ai';
981
1071
  editor.setTool('select');
@@ -1,4 +1,5 @@
1
1
  import type { ExportOptions, InpaintHook, RemoveBackgroundHook, ImageEditorAIHooks, ImageEditorFit } from '@pixerate/editor/image-editor';
2
+ import type { AgentPlayback } from '@pixerate/editor/tools';
2
3
  import { type ReactiveImageEditor } from './imageEditorState.svelte.js';
3
4
  import type { EffectProvider } from '@pixerate/editor/effects';
4
5
  interface Props {
@@ -18,6 +19,15 @@ interface Props {
18
19
  * with an Effects section in the Layers tab. Optional: without one the editor is as before.
19
20
  */
20
21
  effects?: EffectProvider | null;
22
+ /**
23
+ * Agent playback: act out the image tools' calls here with the agent's cursor, opening the
24
+ * panel each tool belongs to (default: `setAgentPlayback` above; `null` turns it off).
25
+ */
26
+ playback?: AgentPlayback | null;
27
+ /** The playback surface id (default `image`, where the image tool providers play). */
28
+ playbackId?: string;
29
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
30
+ onplaybackreveal?: () => void | Promise<void>;
21
31
  }
22
32
  declare const ImageEditor: import("svelte").Component<Props, {}, "">;
23
33
  type ImageEditor = ReturnType<typeof ImageEditor>;
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./history/index.js";
14
14
  export * from "./fileImport.svelte.js";
15
15
  export * from "./docxExport.svelte.js";
16
16
  export * from "./agentTools.svelte.js";
17
+ export * from "./playback/index.js";
17
18
  export * from "./toolBridge.svelte.js";
18
19
  export * from "./toolRegistryView.svelte.js";
19
20
  export * from "./outline.svelte.js";
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export * from "./history/index.js";
5
5
  export * from "./fileImport.svelte.js";
6
6
  export * from "./docxExport.svelte.js";
7
7
  export * from "./agentTools.svelte.js";
8
+ export * from "./playback/index.js";
8
9
  export * from "./toolBridge.svelte.js";
9
10
  export * from "./toolRegistryView.svelte.js";
10
11
  export * from "./outline.svelte.js";
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import type { PresenceStore, PresenceState } from '@pixerate/editor/tools';
4
+
5
+ type Point = { x: number; y: number };
6
+
7
+ interface Props {
8
+ /** The cursors to draw, e.g. from `createPlaybackSurface` or a director. */
9
+ presence: PresenceStore;
10
+ /**
11
+ * How surface coordinates map to this layer's pixels: a canvas viewport
12
+ * (`x * zoom + viewport.x`), or a function. Default: one to one.
13
+ */
14
+ viewport?: { x: number; y: number; zoom: number } | ((point: Point) => Point);
15
+ class?: string;
16
+ }
17
+
18
+ let { presence, viewport, class: className = '' }: Props = $props();
19
+ let view: PresenceState = $state.raw(untrack(() => presence.getState()));
20
+ $effect(() => {
21
+ view = presence.getState();
22
+ return presence.subscribe((next) => {
23
+ view = next;
24
+ });
25
+ });
26
+
27
+ const map = $derived.by((): ((p: Point) => Point) => {
28
+ const v = viewport;
29
+ if (typeof v === 'function') return v;
30
+ if (v) return (p) => ({ x: p.x * v.zoom + v.x, y: p.y * v.zoom + v.y });
31
+ return (p) => p;
32
+ });
33
+
34
+ function ring(focus: { x: number; y: number; width: number; height: number }) {
35
+ const tl = map(focus);
36
+ const br = map({ x: focus.x + focus.width, y: focus.y + focus.height });
37
+ return `left: ${tl.x - 4}px; top: ${tl.y - 4}px; width: ${br.x - tl.x + 8}px; height: ${br.y - tl.y + 8}px;`;
38
+ }
39
+ </script>
40
+
41
+ <!--
42
+ Agent cursors with their name tag or note, and a ring around what each one works on. Place it in
43
+ a `position: relative` box over the surface. The `pixerate-presence` class keeps it out of
44
+ snapshots and exports. Parity with React's `<PresenceLayer>`.
45
+ -->
46
+ {#if view.actors.length}
47
+ <div class="pixerate-presence {className}" aria-hidden="true" data-testid="presence-layer" style="position: absolute; inset: 0; pointer-events: none; overflow: hidden; z-index: 50;">
48
+ {#each view.actors as a (a.id)}
49
+ {#if a.focus}
50
+ <div data-actor-focus={a.id} style="position: absolute; {ring(a.focus)} border-radius: 8px; border: 2px solid {a.color}; background: {a.color}14; transition: all 160ms ease-out;"></div>
51
+ {/if}
52
+ <div data-actor={a.id} style="position: absolute; left: 0; top: 0; transform: translate({map(a).x}px, {map(a).y}px);">
53
+ <svg width="18" height="20" viewBox="0 0 12 18" style="display: block; transform-origin: 0 0; filter: drop-shadow(0 1px 2px rgba(0,0,0,.35)); {a.pressed ? 'transform: scale(0.85);' : ''}">
54
+ <path d="M0,0 L0,14 L4,10.5 L7,17 L9.5,16 L6.5,9.5 L11.5,9.5 Z" style="fill: {a.color}; stroke: #ffffff; stroke-width: 1;" />
55
+ </svg>
56
+ <span style="position: absolute; left: 14px; top: 16px; display: flex; flex-direction: column; align-items: flex-start; gap: 4px;">
57
+ <span style="white-space: nowrap; border-radius: 999px; padding: 1px 7px; font-size: 11px; font-weight: 600; color: #ffffff; background: {a.color};">{a.name}</span>
58
+ {#if a.message}
59
+ <span data-actor-message={a.id} style="width: max-content; max-width: 240px; border-radius: 8px; border-top-left-radius: 0; padding: 4px 8px; font-size: 12px; color: #1e293b; background: #ffffff; box-shadow: 0 4px 12px rgba(15,23,42,.18);">{a.message}</span>
60
+ {/if}
61
+ </span>
62
+ </div>
63
+ {/each}
64
+ </div>
65
+ {/if}
@@ -0,0 +1,22 @@
1
+ import type { PresenceStore } from '@pixerate/editor/tools';
2
+ type Point = {
3
+ x: number;
4
+ y: number;
5
+ };
6
+ interface Props {
7
+ /** The cursors to draw, e.g. from `createPlaybackSurface` or a director. */
8
+ presence: PresenceStore;
9
+ /**
10
+ * How surface coordinates map to this layer's pixels: a canvas viewport
11
+ * (`x * zoom + viewport.x`), or a function. Default: one to one.
12
+ */
13
+ viewport?: {
14
+ x: number;
15
+ y: number;
16
+ zoom: number;
17
+ } | ((point: Point) => Point);
18
+ class?: string;
19
+ }
20
+ declare const PresenceLayer: import("svelte").Component<Props, {}, "">;
21
+ type PresenceLayer = ReturnType<typeof PresenceLayer>;
22
+ export default PresenceLayer;
@@ -0,0 +1,2 @@
1
+ export { default as PresenceLayer } from './PresenceLayer.svelte';
2
+ export { setAgentPlayback, getAgentPlayback, createPlaybackSurface, type PlaybackSurfaceOptions } from './playback.svelte.js';
@@ -0,0 +1,2 @@
1
+ export { default as PresenceLayer } from './PresenceLayer.svelte';
2
+ export { setAgentPlayback, getAgentPlayback, createPlaybackSurface } from './playback.svelte.js';
@@ -0,0 +1,16 @@
1
+ import { PresenceStore, type AgentPlayback, type PlaybackSurface } from '@pixerate/editor/tools';
2
+ type MaybeGetter<T> = T | (() => T);
3
+ /** Makes `playback` the default for every editor surface in this component's subtree (call during init). */
4
+ export declare function setAgentPlayback(playback: AgentPlayback | null | undefined): void;
5
+ /** The nearest `setAgentPlayback` playback, or `null` (call during init). */
6
+ export declare function getAgentPlayback(): AgentPlayback | null;
7
+ /** A surface's behaviour; every function is read fresh on each call. */
8
+ export type PlaybackSurfaceOptions = Omit<PlaybackSurface, 'presence'>;
9
+ /**
10
+ * Attaches a playback surface under `id` (the tool provider's id, or its `playbackSurface`) for the
11
+ * calling component's lifetime (call during init), and returns its cursors to draw with
12
+ * `<PresenceLayer>`. `playback` defaults to `getAgentPlayback()`; with none, nothing is attached and
13
+ * calls run instantly. Parity with React's `usePlaybackSurface`.
14
+ */
15
+ export declare function createPlaybackSurface(playback: MaybeGetter<AgentPlayback | null | undefined>, id: MaybeGetter<string | null | undefined>, options?: MaybeGetter<PlaybackSurfaceOptions>): PresenceStore;
16
+ export {};
@@ -0,0 +1,52 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { PresenceStore } from '@pixerate/editor/tools';
3
+ const KEY = Symbol.for('pixerate.agentPlayback');
4
+ const read = (value) => (typeof value === 'function' ? value() : value);
5
+ /** Makes `playback` the default for every editor surface in this component's subtree (call during init). */
6
+ export function setAgentPlayback(playback) {
7
+ setContext(KEY, playback ?? null);
8
+ }
9
+ /** The nearest `setAgentPlayback` playback, or `null` (call during init). */
10
+ export function getAgentPlayback() {
11
+ return getContext(KEY) ?? null;
12
+ }
13
+ /**
14
+ * Attaches a playback surface under `id` (the tool provider's id, or its `playbackSurface`) for the
15
+ * calling component's lifetime (call during init), and returns its cursors to draw with
16
+ * `<PresenceLayer>`. `playback` defaults to `getAgentPlayback()`; with none, nothing is attached and
17
+ * calls run instantly. Parity with React's `usePlaybackSurface`.
18
+ */
19
+ export function createPlaybackSurface(playback, id, options = {}) {
20
+ const ambient = getAgentPlayback();
21
+ const presence = new PresenceStore();
22
+ const o = () => read(options);
23
+ $effect(() => {
24
+ const explicit = read(playback);
25
+ const target = explicit === undefined ? ambient : explicit;
26
+ const currentId = read(id);
27
+ if (!target || !currentId)
28
+ return;
29
+ const surface = {
30
+ presence,
31
+ locate: (ref) => o().locate?.(ref) ?? null,
32
+ bounds: () => o().bounds?.() ?? null,
33
+ visible: () => o().visible?.() ?? true,
34
+ toViewport: (p) => o().toViewport?.(p) ?? p,
35
+ fromViewport: (p) => o().fromViewport?.(p) ?? p,
36
+ beginGesture: (key) => o().beginGesture?.(key),
37
+ endGesture: () => o().endGesture?.(),
38
+ reveal: () => o().reveal?.(),
39
+ open: (ref) => o().open?.(ref),
40
+ get defaultKind() {
41
+ return o().defaultKind;
42
+ }
43
+ };
44
+ const detach = target.attach(currentId, surface);
45
+ return () => {
46
+ detach();
47
+ for (const a of presence.getState().actors)
48
+ presence.remove(a.id);
49
+ };
50
+ });
51
+ return presence;
52
+ }
@@ -14,8 +14,12 @@
14
14
  toVectorKeyInput,
15
15
  toVectorPointerInput,
16
16
  vectorDiamondPath,
17
+ createVectorPlaybackLocator,
17
18
  type VectorEditorController
18
19
  } from '@pixerate/editor/vector';
20
+ import type { AgentPlayback } from '@pixerate/editor/tools';
21
+ import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
22
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
19
23
  import VectorGradientDefs from './VectorGradientDefs.svelte';
20
24
  import VectorFilterDefs from './VectorFilterDefs.svelte';
21
25
  import VectorGradientAnnotator from './VectorGradientAnnotator.svelte';
@@ -39,6 +43,15 @@
39
43
  style?: string;
40
44
  /** Accessible label (default "Vector canvas"). */
41
45
  'aria-label'?: string;
46
+ /**
47
+ * Agent playback: act out the vector commands' calls here with the agent's cursor (default:
48
+ * `setAgentPlayback` above; `null` turns it off).
49
+ */
50
+ playback?: AgentPlayback | null;
51
+ /** The playback surface id (default `vector`, where the vector commands play). */
52
+ playbackId?: string;
53
+ /** Bring the canvas on screen (switch to its tab) before the agent acts. */
54
+ onplaybackreveal?: () => void | Promise<void>;
42
55
  }
43
56
 
44
57
  let {
@@ -50,7 +63,10 @@
50
63
  idPrefix = `ve${++nextId}-`,
51
64
  class: className = '',
52
65
  style = '',
53
- 'aria-label': ariaLabel = 'Vector canvas'
66
+ 'aria-label': ariaLabel = 'Vector canvas',
67
+ playback,
68
+ playbackId = 'vector',
69
+ onplaybackreveal
54
70
  }: Props = $props();
55
71
 
56
72
  const store = vectorStateOf(() => controller);
@@ -70,6 +86,35 @@
70
86
  const as = THEME.anchorSize;
71
87
 
72
88
  let svg: SVGSVGElement | undefined = $state();
89
+
90
+ // Agent playback: the agent's cursor in document units, drawn through the viewport.
91
+ const ambientPlayback = getAgentPlayback();
92
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
93
+ const svgBox = () => svg?.getBoundingClientRect() ?? null;
94
+ const presence = createPlaybackSurface(() => playing, () => playbackId, () => ({
95
+ defaultKind: 'vector',
96
+ locate: createVectorPlaybackLocator(controller),
97
+ bounds: () => {
98
+ const r = svgBox();
99
+ const v = controller.getState().viewport;
100
+ return r && r.width > 0 ? { x: -v.x / v.zoom, y: -v.y / v.zoom, width: r.width / v.zoom, height: r.height / v.zoom } : null;
101
+ },
102
+ visible: () => {
103
+ const r = svgBox();
104
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
105
+ },
106
+ toViewport: (p) => {
107
+ const r = svgBox();
108
+ const v = controller.getState().viewport;
109
+ return { x: (r?.left ?? 0) + p.x * v.zoom + v.x, y: (r?.top ?? 0) + p.y * v.zoom + v.y };
110
+ },
111
+ fromViewport: (p) => {
112
+ const r = svgBox();
113
+ const v = controller.getState().viewport;
114
+ return { x: (p.x - (r?.left ?? 0) - v.x) / v.zoom, y: (p.y - (r?.top ?? 0) - v.y) / v.zoom };
115
+ },
116
+ reveal: () => onplaybackreveal?.()
117
+ }));
73
118
  $effect(() => {
74
119
  const el = svg;
75
120
  if (!el || typeof ResizeObserver === 'undefined') return;
@@ -611,4 +656,12 @@
611
656
  {/key}
612
657
  </foreignObject>
613
658
  {/if}
659
+ {#if playing}
660
+ <!-- The agent's cursor (agent playback), over everything and never in the way. -->
661
+ <foreignObject data-vector-presence="" x="0" y="0" width="100%" height="100%" pointer-events="none" style="overflow:visible;pointer-events:none">
662
+ <div style="position:relative;width:100%;height:100%">
663
+ <PresenceLayer {presence} viewport={view.viewport} />
664
+ </div>
665
+ </foreignObject>
666
+ {/if}
614
667
  </svg>
@@ -1,4 +1,5 @@
1
1
  import { type VectorEditorController } from '@pixerate/editor/vector';
2
+ import type { AgentPlayback } from '@pixerate/editor/tools';
2
3
  interface Props {
3
4
  controller: VectorEditorController;
4
5
  /** Fit the artboard into the canvas when it first gets a size (default `true`). */
@@ -15,6 +16,15 @@ interface Props {
15
16
  style?: string;
16
17
  /** Accessible label (default "Vector canvas"). */
17
18
  'aria-label'?: string;
19
+ /**
20
+ * Agent playback: act out the vector commands' calls here with the agent's cursor (default:
21
+ * `setAgentPlayback` above; `null` turns it off).
22
+ */
23
+ playback?: AgentPlayback | null;
24
+ /** The playback surface id (default `vector`, where the vector commands play). */
25
+ playbackId?: string;
26
+ /** Bring the canvas on screen (switch to its tab) before the agent acts. */
27
+ onplaybackreveal?: () => void | Promise<void>;
18
28
  }
19
29
  declare const VectorCanvas: import("svelte").Component<Props, {}, "">;
20
30
  type VectorCanvas = ReturnType<typeof VectorCanvas>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.101.4",
3
+ "version": "0.102.0",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -144,7 +144,7 @@
144
144
  "@tiptap/extension-bubble-menu": "^2.27.3",
145
145
  "@tiptap/extension-placeholder": "^2.27.3",
146
146
  "@tiptap/starter-kit": "^2.27.3",
147
- "@pixerate/editor": "0.140.4"
147
+ "@pixerate/editor": "0.141.0"
148
148
  },
149
149
  "peerDependencies": {
150
150
  "@dagrejs/dagre": "^1.1.4",