@pixerate/editor-svelte 0.189.0 → 0.191.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.
@@ -29,6 +29,8 @@
29
29
  problems = r.problems.length ? r.problems.join(' ') : null;
30
30
  if (r.brush !== brush) onchange(r.brush);
31
31
  };
32
+ const setColour = (path: string, light: string, dark: string | null) => set(path, dark === null ? light : { light, dark });
33
+ const setAt = (path: string, list: Array<string | { light: string; dark: string }>, i: number, entry: string | { light: string; dark: string }) => set(path, list.map((x, j) => (j === i ? entry : x)));
32
34
  const save = async () => {
33
35
  try {
34
36
  await onsaveas?.(name.trim());
@@ -71,22 +73,44 @@
71
73
  <input aria-label={c.ariaLabel} value={c.value} placeholder={c.placeholder} onblur={(e) => e.currentTarget.value !== c.value && set(c.path, e.currentTarget.value.trim().toLowerCase())} class="w-28 rounded bg-slate-800 px-1.5 py-0.5" />
72
74
  </label>
73
75
  {:else if c.kind === 'colour'}
74
- <div class="flex items-center justify-between gap-2">
75
- <span class="text-slate-400">{c.label}</span>
76
- <span class="flex items-center gap-1">
77
- <input type="color" aria-label={c.ariaLabel} value={c.value ?? '#000000'} onchange={(e) => set(c.path, e.currentTarget.value)} class="h-5 w-8" />
78
- {#if c.value}<button type="button" class={button} onclick={() => set(c.path, undefined)}>None</button>{/if}
79
- </span>
76
+ <!-- One colour, or a light / dark pair resolved for the host's colour scheme. -->
77
+ <div class="flex flex-col gap-1">
78
+ <div class="flex items-center justify-between gap-2">
79
+ <span class="text-slate-400">{c.label}</span>
80
+ <span class="flex items-center gap-1">
81
+ <input type="color" aria-label={c.ariaLabel} title={c.dark !== null ? 'On light backgrounds' : undefined} value={c.value ?? '#000000'} onchange={(e) => setColour(c.path, e.currentTarget.value, c.dark)} class="h-5 w-8" />
82
+ {#if c.dark !== null}
83
+ <input type="color" aria-label={`${c.ariaLabel} (dark)`} title="On dark backgrounds" value={c.dark} onchange={(e) => setColour(c.path, c.value ?? '#000000', e.currentTarget.value)} class="h-5 w-8" />
84
+ {/if}
85
+ {#if c.value || c.gradient}<button type="button" class={button} onclick={() => set(c.path, undefined)}>None</button>{/if}
86
+ </span>
87
+ </div>
88
+ {#if c.gradient}
89
+ <span class="text-[10px] text-slate-500">A gradient (its own paint).</span>
90
+ {:else}
91
+ <label class="flex items-center gap-2">
92
+ <input type="checkbox" aria-label={`${c.ariaLabel}: light and dark`} checked={c.dark !== null} onchange={(e) => setColour(c.path, c.value ?? '#000000', e.currentTarget.checked ? (c.value ?? '#ffffff') : null)} /> Light / dark
93
+ </label>
94
+ {/if}
80
95
  </div>
81
96
  {:else if c.kind === 'palette'}
82
- <div class="flex flex-wrap items-center gap-1" role="group" aria-label={c.ariaLabel}>
97
+ <div class="flex flex-col gap-1" role="group" aria-label={c.ariaLabel}>
83
98
  <span class="text-slate-400">{c.label}</span>
84
- {#each c.value as col, i (`${col}-${i}`)}
85
- <button type="button" title={`Remove ${col}`} aria-label={`Remove ${col}`} class="h-4 w-4 rounded ring-1 ring-slate-700" style={`background: ${col}`} onclick={() => set(c.path, c.value.filter((_, j) => j !== i))}></button>
86
- {/each}
87
- {#if c.value.length < c.max}
88
- <input type="color" aria-label="Add a palette colour" onchange={(e) => set(c.path, [...c.value, e.currentTarget.value])} class="h-4 w-6" />
89
- {/if}
99
+ <div class="flex flex-wrap items-center gap-1">
100
+ {#each c.value as col, i (i)}
101
+ <span class="flex items-center gap-0.5 rounded bg-slate-900 p-0.5">
102
+ <input type="color" aria-label={`Palette colour ${i + 1}`} value={typeof col === 'string' ? col : col.light} onchange={(e) => setAt(c.path, c.value, i, typeof col === 'string' ? e.currentTarget.value : { ...col, light: e.currentTarget.value })} class="h-4 w-5" />
103
+ {#if typeof col !== 'string'}
104
+ <input type="color" aria-label={`Palette colour ${i + 1} (dark)`} value={col.dark} onchange={(e) => setAt(c.path, c.value, i, { ...col, dark: e.currentTarget.value })} class="h-4 w-5" />
105
+ {/if}
106
+ <button type="button" aria-label={`Remove palette colour ${i + 1}`} class="px-0.5 text-slate-400 hover:text-white" onclick={() => set(c.path, c.value.filter((_, j) => j !== i))}>×</button>
107
+ </span>
108
+ {/each}
109
+ {#if c.value.length < c.max}
110
+ <input type="color" aria-label="Add a palette colour" onchange={(e) => set(c.path, [...c.value, e.currentTarget.value])} class="h-4 w-6" />
111
+ <button type="button" class={button} onclick={() => set(c.path, [...c.value, { light: '#334155', dark: '#e2e8f0' }])}>Add light / dark pair</button>
112
+ {/if}
113
+ </div>
90
114
  </div>
91
115
  {:else if c.kind === 'stamps'}
92
116
  <div class="flex flex-wrap gap-1" role="group" aria-label={c.ariaLabel} title={`Up to ${c.max}`}>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import StickerPicker from './StickerPicker.svelte';
3
3
  import { brushLoadColor, brushTakesPaint, downloadBytes, toggleStickerRef, DEFAULT_PAINT_GRADIENT, type PaintEditorController } from '@pixerate/editor/paint-editor';
4
- import { PAINT_PROFILE, type Brush, type BrushLibraryEntry } from '@pixerate/editor/brushes';
4
+ import { brushColourHex, PAINT_PROFILE, type Brush, type BrushLibraryEntry } from '@pixerate/editor/brushes';
5
5
  import { untrack } from 'svelte';
6
6
  import BrushEditor from './BrushEditor.svelte';
7
7
  import BrushLibraryBrowser from '../brushes/BrushLibraryBrowser.svelte';
@@ -49,6 +49,8 @@
49
49
  return untrack(() => controller.brushThumbnail(brush.id, { width: 240, height: 64, color }));
50
50
  });
51
51
  const carried = $derived(brushLoadColor(snap.mixerLoad));
52
+ // The brush's suggested colours, resolved for the colour scheme: one click makes one the paint colour.
53
+ const palette = $derived((spec.colour?.palette ?? []).flatMap((c) => brushColourHex(c, snap.colorScheme) ?? []));
52
54
  const loads = $derived(['dab', 'mixer', 'impasto'].includes(brush.engine) || (brush.engine === 'wet' && brush.wet.tool === 'paint'));
53
55
  const slug = (name: string) => name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'brush';
54
56
  </script>
@@ -65,7 +67,18 @@
65
67
  {/if}
66
68
  <button type="button" class={button} onclick={() => controller.cleanMixerBrush(brush.engine === 'mixer')}>Clean</button>
67
69
  </div>
68
- {:else if section === 'colour' && brushTakesPaint(brush)}
70
+ {:else if section === 'colour'}
71
+ {#if palette.length}
72
+ <div class="flex flex-wrap items-center gap-1" role="group" aria-label="Paint with a palette colour">
73
+ {#each palette as hex, i (`${hex}-${i}`)}
74
+ <button type="button" aria-label={`Paint with ${hex}`} title={hex} aria-pressed={snap.color === hex} class={`h-5 w-5 rounded ring-1 ${snap.color === hex ? 'ring-white' : 'ring-slate-700'}`} style={`background: ${hex}`} onclick={() => controller.setColor(hex)}></button>
75
+ {/each}
76
+ </div>
77
+ {/if}
78
+ <label class="flex items-center gap-2" title="Choosing a brush with a default colour (the highlighter's yellow) makes it your colour unless this is on">
79
+ <input type="checkbox" aria-label="Keep my colour" checked={snap.colorLocked} onchange={(e) => controller.setColorLocked(e.currentTarget.checked)} /> Keep my colour when choosing a brush
80
+ </label>
81
+ {#if brushTakesPaint(brush)}
69
82
  <div class="flex flex-col gap-1.5" data-testid="brush-gradient">
70
83
  <div class="flex items-center justify-between gap-2">
71
84
  <span class="text-slate-400" title="A gradient of the brush's own, used instead of the paint (the multi-colour brush)">Brush gradient</span>
@@ -75,6 +88,7 @@
75
88
  <PaintGradientEditor value={brush.paint} colors={{ paint: snap.color, secondary: snap.secondaryColor }} onchange={(g) => run(() => controller.setBrush({ paint: g }))} />
76
89
  {/if}
77
90
  </div>
91
+ {/if}
78
92
  {/if}
79
93
  {/snippet}
80
94
 
@@ -11,7 +11,10 @@
11
11
  type PaintEditorController,
12
12
  type PaintPresence,
13
13
  type PaintProposals,
14
+ type PaintAgentStrokes,
15
+ type PaintAgentStrokesState,
14
16
  type PaintTool,
17
+ paintAgentStrokePoints,
15
18
  } from '@pixerate/editor/paint-editor';
16
19
  import { untrack } from 'svelte';
17
20
  import { paintEditorState } from './paintEditorRune.svelte.js';
@@ -33,9 +36,23 @@
33
36
  presence?: PaintPresence;
34
37
  /** Paint an agent suggests, drawn over the canvas as ghosts (see `createPaintProposalState`). */
35
38
  proposals?: PaintProposals;
39
+ /** Strokes an agent is painting live alongside the user's, drawn as soft lines (see `createPaintAgentStrokes`). */
40
+ agentStrokes?: PaintAgentStrokes;
36
41
  class?: string;
37
42
  }
38
- let { controller, tool = 'brush', showGuides = true, fitOnMount = true, onerror, presence, proposals, class: className = '' }: Props = $props();
43
+ let { controller, tool = 'brush', showGuides = true, fitOnMount = true, onerror, presence, proposals, agentStrokes, class: className = '' }: Props = $props();
44
+ let agentLines = $state.raw<PaintAgentStrokesState>({ strokes: [] });
45
+ $effect(() => {
46
+ const store = agentStrokes;
47
+ if (!store) {
48
+ agentLines = { strokes: [] };
49
+ return;
50
+ }
51
+ agentLines = store.getState();
52
+ return store.subscribe((next) => {
53
+ agentLines = next;
54
+ });
55
+ });
39
56
 
40
57
  const editor = paintEditorState(() => controller);
41
58
  const snap = $derived(editor.current);
@@ -198,6 +215,9 @@
198
215
  stroke-dasharray={l.kind === 'symmetry' ? `${6 / snap.view.zoom} ${4 / snap.view.zoom}` : undefined}
199
216
  />
200
217
  {/each}
218
+ {#each agentLines.strokes as s (s.id)}
219
+ <polyline data-agent-stroke={s.id} points={paintAgentStrokePoints(s)} fill="none" stroke={s.color} stroke-opacity={s.opacity ?? 0.6} stroke-width={s.size} stroke-linecap="round" stroke-linejoin="round" />
220
+ {/each}
201
221
  {#if predicted.length > 0}
202
222
  <polyline points={predicted.map((p) => `${p.x},${p.y}`).join(' ')} fill="none" stroke={snap.color} stroke-opacity="0.5" stroke-width={Math.max(1, snap.brush.tip.size / 2)} stroke-linecap="round" />
203
223
  {/if}
@@ -1,4 +1,4 @@
1
- import { type PaintEditorController, type PaintPresence, type PaintProposals, type PaintTool } from '@pixerate/editor/paint-editor';
1
+ import { type PaintEditorController, type PaintPresence, type PaintProposals, type PaintAgentStrokes, type PaintTool } from '@pixerate/editor/paint-editor';
2
2
  interface Props {
3
3
  controller: PaintEditorController;
4
4
  /** The active tool (default brush). */
@@ -13,6 +13,8 @@ interface Props {
13
13
  presence?: PaintPresence;
14
14
  /** Paint an agent suggests, drawn over the canvas as ghosts (see `createPaintProposalState`). */
15
15
  proposals?: PaintProposals;
16
+ /** Strokes an agent is painting live alongside the user's, drawn as soft lines (see `createPaintAgentStrokes`). */
17
+ agentStrokes?: PaintAgentStrokes;
16
18
  class?: string;
17
19
  }
18
20
  declare const PaintCanvas: import("svelte").Component<Props, {}, "">;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { PaintPresence, documentToScreen, screenToDocument, type PaintEditorController, type PaintProposals, type PaintTool } from '@pixerate/editor/paint-editor';
2
+ import { PaintPresence, documentToScreen, screenToDocument, type PaintEditorController, type PaintProposals, type PaintAgentStrokes, type PaintTool } from '@pixerate/editor/paint-editor';
3
3
  import type { AgentPlayback, PlaybackPoint, PlaybackRect, PlaybackRef } from '@pixerate/editor/tools';
4
4
  import { createPlaybackSurface, getAgentPlayback } from '../playback/playback.svelte.js';
5
5
  import PresenceLayer from '../playback/PresenceLayer.svelte';
@@ -25,6 +25,8 @@
25
25
  presence?: PaintPresence;
26
26
  /** Paint an agent suggests, drawn over the canvas as ghosts (see `createPaintProposalState`). */
27
27
  proposals?: PaintProposals;
28
+ /** Strokes an agent is painting live alongside the user's (see `createPaintAgentStrokes`). */
29
+ agentStrokes?: PaintAgentStrokes;
28
30
  class?: string;
29
31
  /**
30
32
  * Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
@@ -37,7 +39,7 @@
37
39
  /** Bring the editor on screen (switch to its tab) before the agent acts. */
38
40
  onplaybackreveal?: () => void | Promise<void>;
39
41
  }
40
- let { controller, fileName, height = 720, presence, proposals, class: className = '', playback, playbackId = 'paint', onplaybackreveal }: Props = $props();
42
+ let { controller, fileName, height = 720, presence, proposals, agentStrokes, class: className = '', playback, playbackId = 'paint', onplaybackreveal }: Props = $props();
41
43
 
42
44
  /** Controls a paint plan can point at, by `{ kind: 'control', id }`. */
43
45
  const PAINT_CONTROLS: Record<string, string> = {
@@ -118,7 +120,7 @@
118
120
  <BrushPanel {controller} class="w-60 shrink-0 overflow-y-auto pr-1" />
119
121
  <div class="flex min-w-0 flex-1 flex-col">
120
122
  <!-- With playback the editor draws the cursor (over the panels too), so the canvas doesn't. -->
121
- <PaintCanvas {controller} {tool} presence={playing ? undefined : presence} {proposals} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
123
+ <PaintCanvas {controller} {tool} presence={playing ? undefined : presence} {proposals} {agentStrokes} onerror={(e) => (error = e)} class="flex-1 rounded-lg" />
122
124
  {#if error}<p role="alert" class="mt-1 text-[11px] text-rose-400">{error}</p>{/if}
123
125
  </div>
124
126
  <div class="flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1">
@@ -1,4 +1,4 @@
1
- import { PaintPresence, type PaintEditorController, type PaintProposals } from '@pixerate/editor/paint-editor';
1
+ import { PaintPresence, type PaintEditorController, type PaintProposals, type PaintAgentStrokes } from '@pixerate/editor/paint-editor';
2
2
  import type { AgentPlayback } from '@pixerate/editor/tools';
3
3
  interface Props {
4
4
  controller: PaintEditorController;
@@ -10,6 +10,8 @@ interface Props {
10
10
  presence?: PaintPresence;
11
11
  /** Paint an agent suggests, drawn over the canvas as ghosts (see `createPaintProposalState`). */
12
12
  proposals?: PaintProposals;
13
+ /** Strokes an agent is painting live alongside the user's (see `createPaintAgentStrokes`). */
14
+ agentStrokes?: PaintAgentStrokes;
13
15
  class?: string;
14
16
  /**
15
17
  * Agent playback: act out the paint tools' calls here with the agent's cursor, opening the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.189.0",
3
+ "version": "0.191.0",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -175,7 +175,7 @@
175
175
  "**/*.css"
176
176
  ],
177
177
  "dependencies": {
178
- "@pixerate/editor": "0.256.0"
178
+ "@pixerate/editor": "0.258.0"
179
179
  },
180
180
  "peerDependencies": {
181
181
  "@dagrejs/dagre": "^1.1.4",