@pixerate/editor-svelte 0.101.4 → 0.103.1

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.
Files changed (33) hide show
  1. package/dist/agentTools.svelte.d.ts +4 -2
  2. package/dist/agentTools.svelte.js +8 -1
  3. package/dist/animation-editor/AnimationEditor.svelte +35 -3
  4. package/dist/animation-editor/AnimationEditor.svelte.d.ts +10 -0
  5. package/dist/canvas/createCanvasPlaybackSurface.svelte.d.ts +30 -0
  6. package/dist/canvas/createCanvasPlaybackSurface.svelte.js +43 -0
  7. package/dist/canvas/index.d.ts +2 -0
  8. package/dist/canvas/index.js +4 -1
  9. package/dist/chart/ChartEditor.svelte +85 -4
  10. package/dist/chart/ChartEditor.svelte.d.ts +11 -0
  11. package/dist/image-editor/ImageEditor.svelte +91 -1
  12. package/dist/image-editor/ImageEditor.svelte.d.ts +10 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +1 -0
  15. package/dist/paint-editor/PaintEditor.svelte +92 -10
  16. package/dist/paint-editor/PaintEditor.svelte.d.ts +12 -1
  17. package/dist/parameters/ParametersPanel.svelte +31 -2
  18. package/dist/parameters/ParametersPanel.svelte.d.ts +10 -0
  19. package/dist/playback/PresenceLayer.svelte +65 -0
  20. package/dist/playback/PresenceLayer.svelte.d.ts +22 -0
  21. package/dist/playback/index.d.ts +2 -0
  22. package/dist/playback/index.js +2 -0
  23. package/dist/playback/playback.svelte.d.ts +32 -0
  24. package/dist/playback/playback.svelte.js +91 -0
  25. package/dist/spreadsheet/SpreadsheetEditor.svelte +63 -3
  26. package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts +10 -0
  27. package/dist/vector/VectorCanvas.svelte +54 -1
  28. package/dist/vector/VectorCanvas.svelte.d.ts +10 -0
  29. package/dist/video-editor/VideoEditor.svelte +8 -3
  30. package/dist/video-editor/VideoEditor.svelte.d.ts +5 -0
  31. package/dist/video-editor/VideoTimeline.svelte +64 -2
  32. package/dist/video-editor/VideoTimeline.svelte.d.ts +10 -0
  33. package/package.json +2 -2
@@ -1,5 +1,8 @@
1
1
  <script lang="ts">
2
- import type { PaintEditorController, PaintPresence, PaintTool } from '@pixerate/editor/paint-editor';
2
+ import { PaintPresence, documentToScreen, screenToDocument, type PaintEditorController, type PaintTool } from '@pixerate/editor/paint-editor';
3
+ import type { AgentPlayback, PlaybackPoint, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
4
+ import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
5
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
3
6
  import BrushPanel from './BrushPanel.svelte';
4
7
  import ColorPanel from './ColorPanel.svelte';
5
8
  import PaintCanvas from './PaintCanvas.svelte';
@@ -20,8 +23,81 @@
20
23
  /** Agent cursors to draw over the canvas (see `createPaintDirector`). */
21
24
  presence?: PaintPresence;
22
25
  class?: string;
26
+ /**
27
+ * Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
28
+ * panel each write belongs to (default: `setAgentPlayback` above; `null` turns it off). With
29
+ * `presence` (a `PaintDirector`'s), the director's strokes share the cursor.
30
+ */
31
+ playback?: AgentPlayback | null;
32
+ /** The playback surface id (default `paint`, where the paint tool providers play). */
33
+ playbackId?: string;
34
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
35
+ onplaybackreveal?: () => void | Promise<void>;
23
36
  }
24
- let { controller, fileName, height = 720, presence, class: className = '' }: Props = $props();
37
+ let { controller, fileName, height = 720, presence, class: className = '', playback, playbackId = 'paint', onplaybackreveal }: Props = $props();
38
+
39
+ /** Controls a paint plan can point at, by `{ kind: 'control', id }`. */
40
+ const PAINT_CONTROLS: Record<string, string> = {
41
+ 'mixing-pad': '[data-testid="mixing-pad"]',
42
+ lighting: '[data-testid="light-panel"]',
43
+ symmetry: '[aria-label="Symmetry"]'
44
+ };
45
+ // Agent playback in document pixels, sharing a director's presence store (one cursor). Panels
46
+ // outside the canvas are mapped into document pixels through the canvas view.
47
+ let root: HTMLDivElement | undefined = $state();
48
+ const ambientPlayback = getAgentPlayback();
49
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
50
+ const viewBox = () => {
51
+ const r = root?.querySelector<HTMLElement>('[data-testid="paint-canvas"]')?.getBoundingClientRect();
52
+ return r && r.width > 0 ? r : null;
53
+ };
54
+ const docToClient = (p: PlaybackPoint): PlaybackPoint | null => {
55
+ const r = viewBox();
56
+ if (!r) return null;
57
+ const q = documentToScreen(controller.getState().view, controller.document.canvas, { width: r.width, height: r.height }, p.x, p.y);
58
+ return { x: r.left + q.x, y: r.top + q.y };
59
+ };
60
+ const clientToDoc = (p: PlaybackPoint): PlaybackPoint | null => {
61
+ const r = viewBox();
62
+ return r ? screenToDocument(controller.getState().view, controller.document.canvas, { width: r.width, height: r.height }, p.x - r.left, p.y - r.top) : null;
63
+ };
64
+ const elementRect = (selector: string): PlaybackRect | null => {
65
+ const r = root?.querySelector(selector)?.getBoundingClientRect();
66
+ if (!r || r.width <= 0) return null;
67
+ // The element's centre and size (the view may rotate; the cursor only needs the centre).
68
+ const c = clientToDoc({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
69
+ const zoom = controller.getState().view.zoom || 1;
70
+ return c ? { x: c.x - r.width / zoom / 2, y: c.y - r.height / zoom / 2, width: r.width / zoom, height: r.height / zoom } : null;
71
+ };
72
+ // svelte-ignore state_referenced_locally
73
+ const sharedPresence = presence ?? new PaintPresence();
74
+ const agentPresence = createPlaybackSurface(() => playing, () => playbackId, () => ({
75
+ locate: (ref: PlaybackRef) => {
76
+ if (ref.kind === 'panel') return elementRect(`[data-paint-tab="${CSS.escape(ref.id)}"]`);
77
+ if (ref.kind === 'panel-body') return elementRect(`[data-paint-panel="${CSS.escape(ref.id)}"]`);
78
+ if (ref.kind === 'control' && PAINT_CONTROLS[ref.id]) return elementRect(PAINT_CONTROLS[ref.id]!);
79
+ return null;
80
+ },
81
+ open: async (ref: PlaybackRef) => {
82
+ if (ref.kind !== 'panel' || tab === ref.id) return;
83
+ root?.querySelector<HTMLElement>(`[data-paint-tab="${CSS.escape(ref.id)}"]`)?.click();
84
+ await new Promise((resolve) => setTimeout(resolve, 60));
85
+ },
86
+ bounds: () => ({ x: 0, y: 0, width: controller.document.canvas.width, height: controller.document.canvas.height }),
87
+ visible: () => {
88
+ const r = root?.getBoundingClientRect();
89
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
90
+ },
91
+ toViewport: (p: PlaybackPoint) => docToClient(p) ?? p,
92
+ fromViewport: (p: PlaybackPoint) => clientToDoc(p) ?? p,
93
+ reveal: () => onplaybackreveal?.()
94
+ }), sharedPresence);
95
+ /** Document pixels → pixels in the editor's root, for the cursor layer. */
96
+ const docToRoot = (p: PlaybackPoint): PlaybackPoint => {
97
+ const c = docToClient(p);
98
+ const box = root?.getBoundingClientRect();
99
+ return c && box ? { x: c.x - box.left, y: c.y - box.top } : p;
100
+ };
25
101
  let tool = $state<PaintTool>('brush');
26
102
  let tab = $state<'color' | 'wet' | 'paper' | 'layers' | 'effects'>('color');
27
103
  const effects = paintEffectsProvider(() => controller);
@@ -30,25 +106,31 @@
30
106
  </script>
31
107
 
32
108
  <!-- Toolbar, brush library, the canvas, and colour / wet media / paper / layers. -->
33
- <div class={`flex flex-col gap-2 rounded-xl border border-slate-800 bg-slate-900 p-2 ${className}`} style={`height: ${typeof height === 'number' ? `${height}px` : height}`} data-testid="paint-editor">
109
+ <div bind:this={root} class={`relative flex flex-col gap-2 rounded-xl border border-slate-800 bg-slate-900 p-2 ${className}`} style={`height: ${typeof height === 'number' ? `${height}px` : height}`} data-testid="paint-editor">
110
+ {#if playing}
111
+ <PresenceLayer presence={agentPresence} viewport={docToRoot} />
112
+ {/if}
34
113
  <PaintToolbar {controller} {tool} ontoolchange={(t) => (tool = t)} {fileName} />
35
114
  <div class="flex min-h-0 flex-1 gap-2">
36
115
  <BrushPanel {controller} class="w-60 shrink-0 overflow-y-auto pr-1" />
37
116
  <div class="flex min-w-0 flex-1 flex-col">
38
- <PaintCanvas {controller} {tool} {presence} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
117
+ <!-- With playback the editor draws the cursor (over the panels too), so the canvas doesn't. -->
118
+ <PaintCanvas {controller} {tool} presence={playing ? undefined : presence} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
39
119
  {#if error}<p role="alert" class="mt-1 text-[11px] text-rose-400">{error}</p>{/if}
40
120
  </div>
41
121
  <div class="flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1">
42
122
  <div class="flex gap-1" role="tablist" aria-label="Paint panels">
43
123
  {#each tabs as t (t)}
44
- <button type="button" role="tab" aria-selected={tab === t} class={chip(tab === t)} onclick={() => (tab = t)}>{t === 'color' ? 'Colour' : t === 'wet' ? 'Wet' : t === 'paper' ? 'Paper' : t === 'layers' ? 'Layers' : 'Effects'}</button>
124
+ <button type="button" role="tab" aria-selected={tab === t} data-paint-tab={t} class={chip(tab === t)} onclick={() => (tab = t)}>{t === 'color' ? 'Colour' : t === 'wet' ? 'Wet' : t === 'paper' ? 'Paper' : t === 'layers' ? 'Layers' : 'Effects'}</button>
45
125
  {/each}
46
126
  </div>
47
- {#if tab === 'color'}<ColorPanel {controller} />{/if}
48
- {#if tab === 'wet'}<WetPanel {controller} />{/if}
49
- {#if tab === 'paper'}<PaperPanel {controller} />{/if}
50
- {#if tab === 'layers'}<PaintLayersPanel {controller} /><StickerPanel {controller} />{/if}
51
- {#if tab === 'effects' && effects.current}<PaintEffectsPanel {controller} />{/if}
127
+ <div class="flex flex-col gap-2" data-paint-panel={tab}>
128
+ {#if tab === 'color'}<ColorPanel {controller} />{/if}
129
+ {#if tab === 'wet'}<WetPanel {controller} />{/if}
130
+ {#if tab === 'paper'}<PaperPanel {controller} />{/if}
131
+ {#if tab === 'layers'}<PaintLayersPanel {controller} /><StickerPanel {controller} />{/if}
132
+ {#if tab === 'effects' && effects.current}<PaintEffectsPanel {controller} />{/if}
133
+ </div>
52
134
  </div>
53
135
  </div>
54
136
  </div>
@@ -1,4 +1,5 @@
1
- import type { PaintEditorController, PaintPresence } from '@pixerate/editor/paint-editor';
1
+ import { PaintPresence, type PaintEditorController } from '@pixerate/editor/paint-editor';
2
+ import type { AgentPlayback } from '@pixerate/editor/tools';
2
3
  interface Props {
3
4
  controller: PaintEditorController;
4
5
  /** Base name of saved and exported files (default `painting`). */
@@ -8,6 +9,16 @@ interface Props {
8
9
  /** Agent cursors to draw over the canvas (see `createPaintDirector`). */
9
10
  presence?: PaintPresence;
10
11
  class?: string;
12
+ /**
13
+ * Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
14
+ * panel each write belongs to (default: `setAgentPlayback` above; `null` turns it off). With
15
+ * `presence` (a `PaintDirector`'s), the director's strokes share the cursor.
16
+ */
17
+ playback?: AgentPlayback | null;
18
+ /** The playback surface id (default `paint`, where the paint tool providers play). */
19
+ playbackId?: string;
20
+ /** Bring the editor on screen (switch to its tab) before the agent acts. */
21
+ onplaybackreveal?: () => void | Promise<void>;
11
22
  }
12
23
  declare const PaintEditor: import("svelte").Component<Props, {}, "">;
13
24
  type PaintEditor = ReturnType<typeof PaintEditor>;
@@ -6,6 +6,9 @@
6
6
 
7
7
  <script lang="ts">
8
8
  import { untrack, type Snippet } from 'svelte';
9
+ import type { AgentPlayback } from '@pixerate/editor/tools';
10
+ import { createElementPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
11
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
9
12
  import {
10
13
  isParameterGroup,
11
14
  parameterLabel,
@@ -40,6 +43,15 @@
40
43
  class?: string;
41
44
  /** Extra content after the fields. */
42
45
  children?: Snippet;
46
+ /**
47
+ * Agent playback: act out `parameters.*` commands 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 `parameters`, where the parameter commands play). */
52
+ playbackId?: string;
53
+ /** Bring the panel on screen (switch to its tab) before the agent acts. */
54
+ onplaybackreveal?: () => void | Promise<void>;
43
55
  }
44
56
 
45
57
  let {
@@ -53,9 +65,22 @@
53
65
  emptyMessage = 'No parameters.',
54
66
  idPrefix = `pixerate-parameters-${++nextId}`,
55
67
  class: className = '',
56
- children
68
+ children,
69
+ playback,
70
+ playbackId = 'parameters',
71
+ onplaybackreveal
57
72
  }: Props = $props();
58
73
 
74
+ // Agent playback: the cursor visits the controls a call changes.
75
+ let root: HTMLDivElement | undefined = $state();
76
+ const ambientPlayback = getAgentPlayback();
77
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
78
+ const agentPresence = createElementPlaybackSurface(() => playing, () => playbackId, () => root, () => ({
79
+ defaultKind: 'parameter',
80
+ selectors: { parameter: (id: string) => `[data-parameter-id="${CSS.escape(id)}"]` },
81
+ reveal: onplaybackreveal
82
+ }));
83
+
59
84
  // Follows a replaced `controller` prop too.
60
85
  let view: ParametersState = $state.raw(untrack(() => controller.getState()));
61
86
  $effect(() => {
@@ -164,7 +189,7 @@
164
189
  keyframe diamonds, per-field reset, presets, and undo / redo / reset / randomize. Slider
165
190
  drags and colour picks are one undo step each.
166
191
  -->
167
- <div class="space-y-3 text-foreground {className}" data-parameters-panel="">
192
+ <div bind:this={root} class="relative space-y-3 text-foreground {className}" data-parameters-panel="">
168
193
  {#if heading || showToolbar}
169
194
  <div class="flex flex-wrap items-center gap-1.5">
170
195
  {#if heading}
@@ -236,4 +261,8 @@
236
261
  {@render items(view.schema.fields)}
237
262
  {/if}
238
263
  {@render children?.()}
264
+ <!-- Last, so it doesn't shift the stacked spacing. -->
265
+ {#if playing}
266
+ <PresenceLayer presence={agentPresence} />
267
+ {/if}
239
268
  </div>
@@ -1,4 +1,5 @@
1
1
  import { type Snippet } from 'svelte';
2
+ import type { AgentPlayback } from '@pixerate/editor/tools';
2
3
  import { type ParametersController } from '@pixerate/editor/parameters';
3
4
  interface Props {
4
5
  controller: ParametersController;
@@ -21,6 +22,15 @@ interface Props {
21
22
  class?: string;
22
23
  /** Extra content after the fields. */
23
24
  children?: Snippet;
25
+ /**
26
+ * Agent playback: act out `parameters.*` commands here with the agent's cursor (default:
27
+ * `setAgentPlayback` above; `null` turns it off).
28
+ */
29
+ playback?: AgentPlayback | null;
30
+ /** The playback surface id (default `parameters`, where the parameter commands play). */
31
+ playbackId?: string;
32
+ /** Bring the panel on screen (switch to its tab) before the agent acts. */
33
+ onplaybackreveal?: () => void | Promise<void>;
24
34
  }
25
35
  declare const ParametersPanel: import("svelte").Component<Props, {}, "">;
26
36
  type ParametersPanel = ReturnType<typeof ParametersPanel>;
@@ -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, createElementPlaybackSurface, type PlaybackSurfaceOptions, type ElementPlaybackSurfaceOptions } from './playback.svelte.js';
@@ -0,0 +1,2 @@
1
+ export { default as PresenceLayer } from './PresenceLayer.svelte';
2
+ export { setAgentPlayback, getAgentPlayback, createPlaybackSurface, createElementPlaybackSurface } from './playback.svelte.js';
@@ -0,0 +1,32 @@
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>,
16
+ /** Use this presence store (e.g. a director's, so its cursor and playback's are one) instead of a new one. */
17
+ sharedPresence?: PresenceStore | null): PresenceStore;
18
+ export interface ElementPlaybackSurfaceOptions {
19
+ /** The kind of a bare `id` / `ids` argument (e.g. `parameter`). */
20
+ defaultKind?: string;
21
+ /** A CSS selector for each ref kind the surface can find inside the element. */
22
+ selectors: Record<string, (id: string) => string>;
23
+ /** Bring the element on screen (switch to its tab) before the agent acts. */
24
+ reveal?: () => void | Promise<void>;
25
+ }
26
+ /**
27
+ * A playback surface for a plain DOM component (call during init): coordinates are pixels of the
28
+ * element's box, and refs are found by the CSS selectors given per kind. Draw the cursors with
29
+ * `<PresenceLayer>` inside the element. Parity with React's `useElementPlaybackSurface`.
30
+ */
31
+ export declare function createElementPlaybackSurface(playback: MaybeGetter<AgentPlayback | null | undefined>, id: MaybeGetter<string | null | undefined>, element: () => HTMLElement | null | undefined, options: MaybeGetter<ElementPlaybackSurfaceOptions>): PresenceStore;
32
+ export {};
@@ -0,0 +1,91 @@
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
+ /** Use this presence store (e.g. a director's, so its cursor and playback's are one) instead of a new one. */
21
+ sharedPresence) {
22
+ const ambient = getAgentPlayback();
23
+ const presence = sharedPresence ?? new PresenceStore();
24
+ const o = () => read(options);
25
+ $effect(() => {
26
+ const explicit = read(playback);
27
+ const target = explicit === undefined ? ambient : explicit;
28
+ const currentId = read(id);
29
+ if (!target || !currentId)
30
+ return;
31
+ const surface = {
32
+ presence,
33
+ locate: (ref) => o().locate?.(ref) ?? null,
34
+ bounds: () => o().bounds?.() ?? null,
35
+ visible: () => o().visible?.() ?? true,
36
+ toViewport: (p) => o().toViewport?.(p) ?? p,
37
+ fromViewport: (p) => o().fromViewport?.(p) ?? p,
38
+ beginGesture: (key) => o().beginGesture?.(key),
39
+ endGesture: () => o().endGesture?.(),
40
+ reveal: () => o().reveal?.(),
41
+ open: (ref) => o().open?.(ref),
42
+ get defaultKind() {
43
+ return o().defaultKind;
44
+ }
45
+ };
46
+ const detach = target.attach(currentId, surface);
47
+ return () => {
48
+ detach();
49
+ for (const a of presence.getState().actors)
50
+ presence.remove(a.id);
51
+ };
52
+ });
53
+ return presence;
54
+ }
55
+ /**
56
+ * A playback surface for a plain DOM component (call during init): coordinates are pixels of the
57
+ * element's box, and refs are found by the CSS selectors given per kind. Draw the cursors with
58
+ * `<PresenceLayer>` inside the element. Parity with React's `useElementPlaybackSurface`.
59
+ */
60
+ export function createElementPlaybackSurface(playback, id, element, options) {
61
+ const box = () => element()?.getBoundingClientRect() ?? null;
62
+ return createPlaybackSurface(playback, id, () => {
63
+ const o = read(options);
64
+ return {
65
+ defaultKind: o.defaultKind,
66
+ locate: (ref) => {
67
+ const selector = o.selectors[ref.kind]?.(ref.id);
68
+ const b = box();
69
+ const r = selector ? element()?.querySelector(selector)?.getBoundingClientRect() : null;
70
+ return b && r && r.width > 0 ? { x: r.left - b.left, y: r.top - b.top, width: r.width, height: r.height } : null;
71
+ },
72
+ bounds: () => {
73
+ const r = box();
74
+ return r && r.width > 0 ? { x: 0, y: 0, width: r.width, height: r.height } : null;
75
+ },
76
+ visible: () => {
77
+ const r = box();
78
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
79
+ },
80
+ toViewport: (p) => {
81
+ const r = box();
82
+ return { x: (r?.left ?? 0) + p.x, y: (r?.top ?? 0) + p.y };
83
+ },
84
+ fromViewport: (p) => {
85
+ const r = box();
86
+ return { x: p.x - (r?.left ?? 0), y: p.y - (r?.top ?? 0) };
87
+ },
88
+ reveal: () => o.reveal?.()
89
+ };
90
+ });
91
+ }
@@ -20,8 +20,12 @@
20
20
  getDataBarCss,
21
21
  conditionalFormatStyleString,
22
22
  type SpreadsheetPanelId,
23
- type SpreadsheetValidationAction
23
+ type SpreadsheetValidationAction,
24
+ parseSpreadsheetPlaybackRef
24
25
  } from '@pixerate/editor/spreadsheet';
26
+ import type { AgentPlayback, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
27
+ import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
28
+ import PresenceLayer from '../playback/PresenceLayer.svelte';
25
29
  import FormulaBar from './FormulaBar.svelte';
26
30
  import SpreadsheetValidationDialog from './SpreadsheetValidationDialog.svelte';
27
31
  import SpreadsheetConditionalFormatPanel from './SpreadsheetConditionalFormatPanel.svelte';
@@ -54,6 +58,15 @@
54
58
  * `true` / `false` force it.
55
59
  */
56
60
  virtualize?: boolean | 'auto';
61
+ /**
62
+ * Agent playback: act out the spreadsheet tools' calls here with the agent's cursor (default:
63
+ * `setAgentPlayback` above; `null` turns it off).
64
+ */
65
+ playback?: AgentPlayback | null;
66
+ /** The playback surface id (default `spreadsheet`, where `createSpreadsheetToolProvider` plays). */
67
+ playbackId?: string;
68
+ /** Bring the grid on screen (switch to its tab) before the agent acts. */
69
+ onplaybackreveal?: () => void | Promise<void>;
57
70
  }
58
71
 
59
72
  let {
@@ -67,7 +80,10 @@
67
80
  onColumnHeaderClick,
68
81
  onColumnResize,
69
82
  showSheetTabs = false,
70
- virtualize = 'auto'
83
+ virtualize = 'auto',
84
+ playback,
85
+ playbackId = 'spreadsheet',
86
+ onplaybackreveal
71
87
  }: Props = $props();
72
88
 
73
89
  // Virtualised rendering: frozen rows/columns, a spacer, the rows and
@@ -120,6 +136,47 @@
120
136
  let isReadOnly = $derived(readOnly || readonly);
121
137
 
122
138
  let gridContainer = $state<HTMLDivElement | null>(null);
139
+
140
+ // Agent playback: the agent's cursor in pixels of the grid's box, finding cells by data-row / data-col.
141
+ const ambientPlayback = getAgentPlayback();
142
+ const playing = $derived((playback === undefined ? ambientPlayback : playback) ?? null);
143
+ const agentPresence = createPlaybackSurface(() => playing, () => playbackId, () => ({
144
+ defaultKind: 'cell',
145
+ locate: (ref: PlaybackRef): PlaybackRect | null => {
146
+ const grid = gridContainer;
147
+ const range = parseSpreadsheetPlaybackRef(ref);
148
+ if (!grid || !range) return null;
149
+ const box = grid.getBoundingClientRect();
150
+ const inside = (v: number, a: number | null, b: number | null) => (a === null || v >= a) && (b === null || v <= b);
151
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
152
+ for (const td of grid.querySelectorAll<HTMLElement>('td[data-row][data-col]')) {
153
+ if (!inside(Number(td.dataset.row), range.startRow, range.endRow) || !inside(Number(td.dataset.col), range.startCol, range.endCol)) continue;
154
+ const r = td.getBoundingClientRect();
155
+ minX = Math.min(minX, r.left - box.left);
156
+ minY = Math.min(minY, r.top - box.top);
157
+ maxX = Math.max(maxX, r.right - box.left);
158
+ maxY = Math.max(maxY, r.bottom - box.top);
159
+ }
160
+ return minX < maxX ? { x: minX, y: minY, width: maxX - minX, height: maxY - minY } : null;
161
+ },
162
+ bounds: () => {
163
+ const r = gridContainer?.getBoundingClientRect();
164
+ return r && r.width > 0 ? { x: 0, y: 0, width: r.width, height: r.height } : null;
165
+ },
166
+ visible: () => {
167
+ const r = gridContainer?.getBoundingClientRect();
168
+ return !!r && r.width > 0 && r.height > 0 && !(typeof document !== 'undefined' && document.visibilityState === 'hidden');
169
+ },
170
+ toViewport: (p: { x: number; y: number }) => {
171
+ const r = gridContainer?.getBoundingClientRect();
172
+ return { x: (r?.left ?? 0) + p.x, y: (r?.top ?? 0) + p.y };
173
+ },
174
+ fromViewport: (p: { x: number; y: number }) => {
175
+ const r = gridContainer?.getBoundingClientRect();
176
+ return { x: p.x - (r?.left ?? 0), y: p.y - (r?.top ?? 0) };
177
+ },
178
+ reveal: () => onplaybackreveal?.()
179
+ }));
123
180
  let cellInput = $state<HTMLInputElement | null>(null);
124
181
 
125
182
  // Focus the cell editor when it opens, with the caret after the draft
@@ -586,7 +643,7 @@
586
643
  {#snippet grid()}
587
644
  <div
588
645
  bind:this={gridContainer}
589
- class="flex flex-col h-full w-full overflow-hidden border border-border bg-background select-none outline-none text-foreground font-sans text-xs {className}"
646
+ class="relative flex flex-col h-full w-full overflow-hidden border border-border bg-background select-none outline-none text-foreground font-sans text-xs {className}"
590
647
  tabindex="0"
591
648
  onkeydown={handleKeyDown}
592
649
  role="grid"
@@ -879,6 +936,9 @@
879
936
  {#if sheetState.validationPrompt}
880
937
  <SpreadsheetValidationDialog prompt={sheetState.validationPrompt} onResolve={resolveValidation} />
881
938
  {/if}
939
+ {#if playing}
940
+ <PresenceLayer presence={agentPresence} />
941
+ {/if}
882
942
  </div>
883
943
  {/snippet}
884
944
 
@@ -1,5 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import { type SpreadsheetColumn, type SpreadsheetRow, type CellData } from '@pixerate/editor/spreadsheet';
3
+ import type { AgentPlayback } from '@pixerate/editor/tools';
3
4
  import { createReactiveSpreadsheet } from './spreadsheet.svelte.js';
4
5
  interface Props {
5
6
  sheetState?: ReturnType<typeof createReactiveSpreadsheet>;
@@ -23,6 +24,15 @@ interface Props {
23
24
  * `true` / `false` force it.
24
25
  */
25
26
  virtualize?: boolean | 'auto';
27
+ /**
28
+ * Agent playback: act out the spreadsheet tools' calls here with the agent's cursor (default:
29
+ * `setAgentPlayback` above; `null` turns it off).
30
+ */
31
+ playback?: AgentPlayback | null;
32
+ /** The playback surface id (default `spreadsheet`, where `createSpreadsheetToolProvider` plays). */
33
+ playbackId?: string;
34
+ /** Bring the grid on screen (switch to its tab) before the agent acts. */
35
+ onplaybackreveal?: () => void | Promise<void>;
26
36
  }
27
37
  declare const SpreadsheetEditor: import("svelte").Component<Props, {}, "">;
28
38
  type SpreadsheetEditor = ReturnType<typeof SpreadsheetEditor>;