@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.
- package/dist/agentTools.svelte.d.ts +4 -2
- package/dist/agentTools.svelte.js +8 -1
- package/dist/animation-editor/AnimationEditor.svelte +35 -3
- package/dist/animation-editor/AnimationEditor.svelte.d.ts +10 -0
- package/dist/canvas/createCanvasPlaybackSurface.svelte.d.ts +30 -0
- package/dist/canvas/createCanvasPlaybackSurface.svelte.js +43 -0
- package/dist/canvas/index.d.ts +2 -0
- package/dist/canvas/index.js +4 -1
- package/dist/chart/ChartEditor.svelte +85 -4
- package/dist/chart/ChartEditor.svelte.d.ts +11 -0
- package/dist/image-editor/ImageEditor.svelte +91 -1
- package/dist/image-editor/ImageEditor.svelte.d.ts +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/paint-editor/PaintEditor.svelte +92 -10
- package/dist/paint-editor/PaintEditor.svelte.d.ts +12 -1
- package/dist/parameters/ParametersPanel.svelte +31 -2
- package/dist/parameters/ParametersPanel.svelte.d.ts +10 -0
- package/dist/playback/PresenceLayer.svelte +65 -0
- package/dist/playback/PresenceLayer.svelte.d.ts +22 -0
- package/dist/playback/index.d.ts +2 -0
- package/dist/playback/index.js +2 -0
- package/dist/playback/playback.svelte.d.ts +32 -0
- package/dist/playback/playback.svelte.js +91 -0
- package/dist/spreadsheet/SpreadsheetEditor.svelte +63 -3
- package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts +10 -0
- package/dist/vector/VectorCanvas.svelte +54 -1
- package/dist/vector/VectorCanvas.svelte.d.ts +10 -0
- package/dist/video-editor/VideoEditor.svelte +8 -3
- package/dist/video-editor/VideoEditor.svelte.d.ts +5 -0
- package/dist/video-editor/VideoTimeline.svelte +64 -2
- package/dist/video-editor/VideoTimeline.svelte.d.ts +10 -0
- package/package.json +2 -2
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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,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>;
|