@pixerate/editor-svelte 0.108.0 → 0.109.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.
- package/dist/canvas/createCanvasPlaybackSurface.svelte.d.ts +8 -2
- package/dist/canvas/createCanvasPlaybackSurface.svelte.js +13 -8
- package/dist/image-editor/AdjustmentLayersPanel.svelte +27 -26
- package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts +2 -0
- package/dist/image-editor/BrushOptionsBar.svelte +35 -34
- package/dist/image-editor/ColorProfilePanel.svelte +12 -12
- package/dist/image-editor/ControlSlider.svelte +6 -4
- package/dist/image-editor/ControlSlider.svelte.d.ts +2 -0
- package/dist/image-editor/CurvesEditor.svelte +6 -4
- package/dist/image-editor/CurvesEditor.svelte.d.ts +2 -0
- package/dist/image-editor/FreeTransformOptionsBar.svelte +22 -21
- package/dist/image-editor/Histogram.svelte +1 -1
- package/dist/image-editor/ImageEditor.svelte +127 -142
- package/dist/image-editor/ImageEffectControls.svelte +15 -11
- package/dist/image-editor/ImageEffectControls.svelte.d.ts +2 -0
- package/dist/image-editor/ImageProviderEffectsPanel.svelte +15 -15
- package/dist/image-editor/LayerEffectsPanel.svelte +22 -23
- package/dist/image-editor/LayersPanel.svelte +39 -39
- package/dist/image-editor/RefineEdgePanel.svelte +14 -13
- package/dist/image-editor/SelectionOptionsBar.svelte +12 -11
- package/dist/image-editor/ShapeOptionsBar.svelte +14 -13
- package/dist/image-editor/TypeOptionsBar.svelte +12 -11
- package/dist/image-editor/styles.d.ts +51 -0
- package/dist/image-editor/styles.js +51 -0
- package/dist/playback/playback.svelte.d.ts +7 -4
- package/dist/playback/playback.svelte.js +17 -9
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { imageRangeClass } from './styles.js';
|
|
2
3
|
import type {
|
|
3
4
|
ImageEditorController,
|
|
4
5
|
SelectionMask,
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
|
|
57
58
|
const btn = (on: boolean) =>
|
|
58
59
|
`px-2 py-1 rounded-md border text-[11px] transition-colors ${
|
|
59
|
-
on ? 'bg-
|
|
60
|
+
on ? 'bg-primary border-primary text-primary-foreground' : 'bg-background border-border text-foreground hover:bg-accent hover:text-accent-foreground'
|
|
60
61
|
}`;
|
|
61
62
|
</script>
|
|
62
63
|
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
{#if options.tool === 'wand'}
|
|
79
80
|
<div class="flex flex-col gap-2">
|
|
80
81
|
<label class="flex flex-col gap-1">
|
|
81
|
-
<span class="flex justify-between text-
|
|
82
|
+
<span class="flex justify-between text-muted-foreground">
|
|
82
83
|
<span>Tolerance</span>
|
|
83
84
|
<span class="font-mono">{options.tolerance}</span>
|
|
84
85
|
</span>
|
|
@@ -90,24 +91,24 @@
|
|
|
90
91
|
aria-label="Tolerance"
|
|
91
92
|
value={options.tolerance}
|
|
92
93
|
oninput={(e) => set({ tolerance: Number((e.currentTarget as HTMLInputElement).value) })}
|
|
93
|
-
class=
|
|
94
|
+
class={imageRangeClass}
|
|
94
95
|
/>
|
|
95
96
|
</label>
|
|
96
|
-
<label class="flex items-center gap-2 text-
|
|
97
|
+
<label class="flex items-center gap-2 text-foreground">
|
|
97
98
|
<input type="checkbox" checked={options.contiguous} onchange={(e) => set({ contiguous: (e.currentTarget as HTMLInputElement).checked })} />
|
|
98
99
|
Contiguous
|
|
99
100
|
</label>
|
|
100
|
-
<label class="flex items-center gap-2 text-
|
|
101
|
+
<label class="flex items-center gap-2 text-foreground">
|
|
101
102
|
<input type="checkbox" checked={options.sampleMerged} onchange={(e) => set({ sampleMerged: (e.currentTarget as HTMLInputElement).checked })} />
|
|
102
103
|
Sample adjusted image
|
|
103
104
|
</label>
|
|
104
|
-
<label class="flex items-center gap-2 text-
|
|
105
|
+
<label class="flex items-center gap-2 text-foreground">
|
|
105
106
|
Sample
|
|
106
107
|
<select
|
|
107
108
|
aria-label="Sample from"
|
|
108
109
|
value={options.sample ?? 'composite'}
|
|
109
110
|
onchange={(e) => set({ sample: (e.currentTarget as HTMLSelectElement).value as 'composite' | 'active-layer' })}
|
|
110
|
-
class="bg-
|
|
111
|
+
class="bg-muted border border-border rounded px-1 py-0.5"
|
|
111
112
|
>
|
|
112
113
|
<option value="composite">All layers</option>
|
|
113
114
|
<option value="active-layer">Active layer</option>
|
|
@@ -115,7 +116,7 @@
|
|
|
115
116
|
</label>
|
|
116
117
|
</div>
|
|
117
118
|
{/if}
|
|
118
|
-
<label class="flex items-center gap-2 text-
|
|
119
|
+
<label class="flex items-center gap-2 text-foreground">
|
|
119
120
|
<input type="checkbox" checked={options.antiAlias} onchange={(e) => set({ antiAlias: (e.currentTarget as HTMLInputElement).checked })} />
|
|
120
121
|
Anti-alias
|
|
121
122
|
</label>
|
|
@@ -126,7 +127,7 @@
|
|
|
126
127
|
<button type="button" aria-pressed={qm} class={btn(qm)} onclick={() => controller.setQuickMask(!qm)}>Quick Mask</button>
|
|
127
128
|
</div>
|
|
128
129
|
<div class="flex flex-wrap items-center gap-1">
|
|
129
|
-
<label class="flex items-center gap-1 text-
|
|
130
|
+
<label class="flex items-center gap-1 text-muted-foreground">
|
|
130
131
|
Radius
|
|
131
132
|
<input
|
|
132
133
|
type="number"
|
|
@@ -136,12 +137,12 @@
|
|
|
136
137
|
aria-label="Modify radius"
|
|
137
138
|
value={radius}
|
|
138
139
|
oninput={(e) => (radius = Math.max(0, Number((e.currentTarget as HTMLInputElement).value) || 0))}
|
|
139
|
-
class="w-14 bg-
|
|
140
|
+
class="w-14 bg-background border border-border rounded px-1 py-0.5 text-foreground"
|
|
140
141
|
/>
|
|
141
142
|
</label>
|
|
142
143
|
{#each MODIFY_OPS as m (m.op)}
|
|
143
144
|
<button type="button" class={btn(false)} disabled={!sel} onclick={() => controller.modifySelection(m.op, radius)}>{m.label}</button>
|
|
144
145
|
{/each}
|
|
145
146
|
</div>
|
|
146
|
-
<p data-testid="selection-status" class="text-
|
|
147
|
+
<p data-testid="selection-status" class="text-muted-foreground">{describeSelection(sel)}</p>
|
|
147
148
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { imageChipClass, imageColorInputClass, imageFieldClass } from './styles.js';
|
|
2
3
|
import type { ImageEditorController, ShapeToolOptions } from '@pixerate/editor/image-editor';
|
|
3
4
|
import { LIVE_SHAPE_KINDS, SHAPE_KIND_LABELS, applyShapeOptions } from '@pixerate/editor/image-editor';
|
|
4
5
|
import { untrack } from 'svelte';
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
});
|
|
30
31
|
let error = $state<string | null>(null);
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
const num = (v: string, min: number, max: number) => Math.max(min, Math.min(max, Number(v) || min));
|
|
34
35
|
|
|
35
36
|
function set(change: Partial<ShapeToolOptions>) {
|
|
@@ -46,10 +47,10 @@
|
|
|
46
47
|
const checked = (e: Event) => (e.target as HTMLInputElement).checked;
|
|
47
48
|
</script>
|
|
48
49
|
|
|
49
|
-
<div class="flex flex-col gap-2 text-xs text-
|
|
50
|
+
<div class="flex flex-col gap-2 text-xs text-foreground {className}" data-testid="shape-options">
|
|
50
51
|
<div class="flex flex-wrap gap-1" role="toolbar" aria-label="Shape kind">
|
|
51
52
|
{#each LIVE_SHAPE_KINDS as k (k)}
|
|
52
|
-
<button type="button" aria-pressed={options.kind === k} class={
|
|
53
|
+
<button type="button" aria-pressed={options.kind === k} class={imageChipClass(options.kind === k)} onclick={() => onchange({ ...options, kind: k })}>{SHAPE_KIND_LABELS[k]}</button>
|
|
53
54
|
{/each}
|
|
54
55
|
</div>
|
|
55
56
|
<div class="flex flex-wrap items-center gap-3">
|
|
@@ -62,7 +63,7 @@
|
|
|
62
63
|
disabled={options.fill === null}
|
|
63
64
|
value={options.fill ?? '#4f46e5'}
|
|
64
65
|
oninput={(e) => set({ fill: value(e) })}
|
|
65
|
-
class=
|
|
66
|
+
class={imageColorInputClass}
|
|
66
67
|
/>
|
|
67
68
|
</label>
|
|
68
69
|
<label class="flex items-center gap-1">
|
|
@@ -74,38 +75,38 @@
|
|
|
74
75
|
disabled={options.stroke === null}
|
|
75
76
|
value={options.stroke ?? '#000000'}
|
|
76
77
|
oninput={(e) => set({ stroke: value(e) })}
|
|
77
|
-
class=
|
|
78
|
+
class={imageColorInputClass}
|
|
78
79
|
/>
|
|
79
80
|
</label>
|
|
80
81
|
<label class="flex items-center gap-1">
|
|
81
82
|
Width
|
|
82
|
-
<input type="number" aria-label="Stroke width" min="0" max="200" value={options.strokeWidth} oninput={(e) => set({ strokeWidth: num(value(e), 0, 200) })} class="w-14
|
|
83
|
+
<input type="number" aria-label="Stroke width" min="0" max="200" value={options.strokeWidth} oninput={(e) => set({ strokeWidth: num(value(e), 0, 200) })} class="w-14 {imageFieldClass}" />
|
|
83
84
|
</label>
|
|
84
85
|
</div>
|
|
85
86
|
<div class="flex flex-wrap items-center gap-3">
|
|
86
87
|
{#if options.kind === 'rect'}
|
|
87
88
|
<label class="flex items-center gap-1">
|
|
88
89
|
Radius
|
|
89
|
-
<input type="number" aria-label="Corner radius" min="0" max="1000" value={options.radius} oninput={(e) => set({ radius: num(value(e), 0, 1000) })} class="w-14
|
|
90
|
+
<input type="number" aria-label="Corner radius" min="0" max="1000" value={options.radius} oninput={(e) => set({ radius: num(value(e), 0, 1000) })} class="w-14 {imageFieldClass}" />
|
|
90
91
|
</label>
|
|
91
92
|
{:else if options.kind === 'polygon'}
|
|
92
93
|
<label class="flex items-center gap-1">
|
|
93
94
|
Sides
|
|
94
|
-
<input type="number" aria-label="Sides" min="3" max="100" value={options.sides} oninput={(e) => set({ sides: num(value(e), 3, 100) })} class="w-14
|
|
95
|
+
<input type="number" aria-label="Sides" min="3" max="100" value={options.sides} oninput={(e) => set({ sides: num(value(e), 3, 100) })} class="w-14 {imageFieldClass}" />
|
|
95
96
|
</label>
|
|
96
97
|
{:else if options.kind === 'star'}
|
|
97
98
|
<label class="flex items-center gap-1">
|
|
98
99
|
Points
|
|
99
|
-
<input type="number" aria-label="Points" min="3" max="100" value={options.points} oninput={(e) => set({ points: num(value(e), 3, 100) })} class="w-14
|
|
100
|
+
<input type="number" aria-label="Points" min="3" max="100" value={options.points} oninput={(e) => set({ points: num(value(e), 3, 100) })} class="w-14 {imageFieldClass}" />
|
|
100
101
|
</label>
|
|
101
102
|
<label class="flex items-center gap-1">
|
|
102
103
|
Inner
|
|
103
|
-
<input type="range" aria-label="Inner radius" min="0.05" max="1" step="0.05" value={options.innerRatio} oninput={(e) => set({ innerRatio: Number(value(e)) })} class="accent-
|
|
104
|
+
<input type="range" aria-label="Inner radius" min="0.05" max="1" step="0.05" value={options.innerRatio} oninput={(e) => set({ innerRatio: Number(value(e)) })} class="accent-primary" />
|
|
104
105
|
</label>
|
|
105
106
|
{:else if options.kind === 'line'}
|
|
106
107
|
<label class="flex items-center gap-1">
|
|
107
108
|
Weight
|
|
108
|
-
<input type="number" aria-label="Line weight" min="1" max="200" value={options.weight} oninput={(e) => set({ weight: num(value(e), 1, 200) })} class="w-14
|
|
109
|
+
<input type="number" aria-label="Line weight" min="1" max="200" value={options.weight} oninput={(e) => set({ weight: num(value(e), 1, 200) })} class="w-14 {imageFieldClass}" />
|
|
109
110
|
</label>
|
|
110
111
|
<label class="flex items-center gap-1">
|
|
111
112
|
<input type="checkbox" aria-label="Arrowhead" checked={options.arrow} onchange={(e) => set({ arrow: checked(e) })} />
|
|
@@ -113,8 +114,8 @@
|
|
|
113
114
|
</label>
|
|
114
115
|
{/if}
|
|
115
116
|
</div>
|
|
116
|
-
<p class="text-[10px] text-
|
|
117
|
+
<p class="text-[10px] text-muted-foreground">Drag on the image to draw. Shift: square, circle or 45° line. Alt: from the centre.</p>
|
|
117
118
|
{#if error}
|
|
118
|
-
<p role="alert" class="text-
|
|
119
|
+
<p role="alert" class="text-destructive">{error}</p>
|
|
119
120
|
{/if}
|
|
120
121
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { imageChipClass, imageColorInputClass } from './styles.js';
|
|
2
3
|
import type { ImageEditorController, TextAlign, TextEditSession, TextStyle, TypeToolOptions } from '@pixerate/editor/image-editor';
|
|
3
4
|
import { TEXT_ALIGNS } from '@pixerate/editor/image-editor';
|
|
4
5
|
import { untrack } from 'svelte';
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
return untrack(() => s.subscribe(() => tick++));
|
|
31
32
|
});
|
|
32
33
|
const ALIGN_LABELS: Record<TextAlign, string> = { left: 'Left', center: 'Centre', right: 'Right', justify: 'Justify' };
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
|
|
35
36
|
const view = $derived.by(() => {
|
|
36
37
|
void tick;
|
|
@@ -74,11 +75,11 @@
|
|
|
74
75
|
const value = (e: Event) => (e.target as HTMLInputElement).value;
|
|
75
76
|
</script>
|
|
76
77
|
|
|
77
|
-
<div class="flex flex-col gap-2 text-xs text-
|
|
78
|
+
<div class="flex flex-col gap-2 text-xs text-foreground {className}" data-testid="type-options">
|
|
78
79
|
<div class="flex flex-wrap items-center gap-2">
|
|
79
80
|
<label class="flex items-center gap-1">
|
|
80
81
|
Font
|
|
81
|
-
<input list="pixerate-type-fonts" aria-label="Font family" value={view.shown.fontFamily} oninput={(e) => value(e).trim() && style({ fontFamily: value(e) })} class="w-32 rounded border border-
|
|
82
|
+
<input list="pixerate-type-fonts" aria-label="Font family" value={view.shown.fontFamily} oninput={(e) => value(e).trim() && style({ fontFamily: value(e) })} class="w-32 rounded border border-border bg-background px-1 py-0.5" />
|
|
82
83
|
<datalist id="pixerate-type-fonts">
|
|
83
84
|
{#each view.fonts as f (f)}
|
|
84
85
|
<option value={f}></option>
|
|
@@ -94,23 +95,23 @@
|
|
|
94
95
|
max="2000"
|
|
95
96
|
value={Math.round(view.shown.fontSize * 10) / 10}
|
|
96
97
|
oninput={(e) => Number(value(e)) > 0 && style({ fontSize: Number(value(e)) })}
|
|
97
|
-
class="w-16 rounded border border-
|
|
98
|
+
class="w-16 rounded border border-border bg-background px-1 py-0.5"
|
|
98
99
|
/>
|
|
99
100
|
</label>
|
|
100
|
-
<button type="button" aria-pressed={view.shown.fontWeight >= 600} class="{
|
|
101
|
-
<button type="button" aria-pressed={view.shown.italic} class="{
|
|
102
|
-
<input type="color" aria-label="Text colour" value={view.shown.color} oninput={(e) => style({ color: value(e) })} class=
|
|
101
|
+
<button type="button" aria-pressed={view.shown.fontWeight >= 600} class="{imageChipClass(view.shown.fontWeight >= 600)} font-bold" onclick={() => style({ fontWeight: view.shown.fontWeight >= 600 ? 400 : 700 })}>B</button>
|
|
102
|
+
<button type="button" aria-pressed={view.shown.italic} class="{imageChipClass(view.shown.italic)} italic" onclick={() => style({ italic: !view.shown.italic })}>I</button>
|
|
103
|
+
<input type="color" aria-label="Text colour" value={view.shown.color} oninput={(e) => style({ color: value(e) })} class={imageColorInputClass} />
|
|
103
104
|
</div>
|
|
104
105
|
<div class="flex gap-1" role="toolbar" aria-label="Alignment">
|
|
105
106
|
{#each TEXT_ALIGNS as a (a)}
|
|
106
|
-
<button type="button" aria-pressed={options.align === a} class={
|
|
107
|
+
<button type="button" aria-pressed={options.align === a} class={imageChipClass(options.align === a)} onclick={() => style({ align: a })}>{ALIGN_LABELS[a]}</button>
|
|
107
108
|
{/each}
|
|
108
109
|
</div>
|
|
109
|
-
<p class="text-[10px] text-
|
|
110
|
+
<p class="text-[10px] text-muted-foreground">Click to type point text, drag for a paragraph box, click text to edit it. Esc finishes.</p>
|
|
110
111
|
{#if view.missing.length > 0}
|
|
111
|
-
<p role="status" class="text-amber-
|
|
112
|
+
<p role="status" class="text-amber-600" data-testid="type-missing-fonts">Missing {view.missing.length === 1 ? 'font' : 'fonts'}: {view.missing.join(', ')}</p>
|
|
112
113
|
{/if}
|
|
113
114
|
{#if error}
|
|
114
|
-
<p role="alert" class="text-
|
|
115
|
+
<p role="alert" class="text-destructive">{error}</p>
|
|
115
116
|
{/if}
|
|
116
117
|
</div>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind classes shared by the image editor and its panels (same in the React and Svelte
|
|
3
|
+
* packages). They use shadcn theme tokens (`background`, `foreground`, `muted`, `accent`,
|
|
4
|
+
* `border`, `input`, `primary`, `destructive`), so the editor follows the host's light or dark theme.
|
|
5
|
+
*/
|
|
6
|
+
/** The editor's frame. */
|
|
7
|
+
export declare const imageEditorFrameClass = "relative flex flex-col bg-background border border-border rounded-xl overflow-hidden shadow-sm text-foreground";
|
|
8
|
+
/** The bar above the canvas (tabs and global actions). */
|
|
9
|
+
export declare const imageEditorHeaderClass = "flex flex-wrap items-center justify-between px-4 py-3 bg-muted/40 border-b border-border gap-3";
|
|
10
|
+
/** The tab strip. */
|
|
11
|
+
export declare const imageTabListClass = "flex items-center gap-1 bg-background p-1 rounded-lg border border-border text-xs";
|
|
12
|
+
/** A tab of the editor, highlighted when `active`. */
|
|
13
|
+
export declare const imageTabClass: (active: boolean) => string;
|
|
14
|
+
/** A bordered button in the header (undo, redo, zoom…). */
|
|
15
|
+
export declare const imageHeaderButtonClass = "p-1.5 rounded-lg border border-border bg-background text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed";
|
|
16
|
+
/** The primary action (export, apply). */
|
|
17
|
+
export declare const imagePrimaryButtonClass = "rounded-lg bg-primary hover:bg-primary/90 text-primary-foreground font-medium shadow transition-colors disabled:opacity-50";
|
|
18
|
+
/** The side panel next to the canvas. */
|
|
19
|
+
export declare const imageSidebarClass = "w-full md:w-80 bg-background border-t md:border-t-0 md:border-l border-border p-4 overflow-y-auto h-auto md:h-full md:max-h-[560px]";
|
|
20
|
+
/** The canvas area's background. */
|
|
21
|
+
export declare const imageCanvasAreaClass = "bg-muted";
|
|
22
|
+
/** A small uppercase section title. */
|
|
23
|
+
export declare const imageSectionTitleClass = "font-semibold text-foreground uppercase tracking-wider text-[11px]";
|
|
24
|
+
/** A section under a rule. */
|
|
25
|
+
export declare const imageSectionClass = "border-t border-border pt-3";
|
|
26
|
+
/** A thin rule between groups of controls. */
|
|
27
|
+
export declare const imageDividerClass = "h-px bg-border my-1";
|
|
28
|
+
/** A small secondary button. */
|
|
29
|
+
export declare const imageSmallButtonClass = "px-2 py-1 rounded text-[11px] bg-muted text-foreground hover:bg-accent hover:text-accent-foreground self-start disabled:opacity-40";
|
|
30
|
+
/** A tiny button in a row (layer actions, effect actions). */
|
|
31
|
+
export declare const imageIconButtonClass = "px-1.5 py-0.5 rounded text-[10px] bg-muted text-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-40";
|
|
32
|
+
/** An outlined tiny button. */
|
|
33
|
+
export declare const imageOutlineButtonClass = "rounded border border-border px-1.5 py-0.5 text-[11px] text-foreground hover:bg-muted disabled:opacity-40";
|
|
34
|
+
/** A toggle chip, filled when `on`. */
|
|
35
|
+
export declare const imageChipClass: (on: boolean) => string;
|
|
36
|
+
/** A bordered choice button, highlighted when `on`. */
|
|
37
|
+
export declare const imageChoiceClass: (on: boolean) => string;
|
|
38
|
+
/** A text or number field. */
|
|
39
|
+
export declare const imageFieldClass = "rounded border border-input bg-background px-1 py-0.5 text-foreground";
|
|
40
|
+
/** A select. */
|
|
41
|
+
export declare const imageSelectClass = "rounded border border-input bg-background px-1.5 py-1 text-foreground";
|
|
42
|
+
/** A colour input swatch. */
|
|
43
|
+
export declare const imageColorInputClass = "h-6 w-8 rounded border border-border bg-transparent";
|
|
44
|
+
/** A full-width range slider. */
|
|
45
|
+
export declare const imageRangeClass = "w-full accent-primary";
|
|
46
|
+
/** An error message. */
|
|
47
|
+
export declare const imageErrorClass = "text-destructive";
|
|
48
|
+
/** An error banner. */
|
|
49
|
+
export declare const imageErrorBannerClass = "border-b border-destructive/40 bg-destructive/10 px-4 py-1.5 text-xs text-destructive";
|
|
50
|
+
/** A spinner on a primary button. */
|
|
51
|
+
export declare const imageSpinnerClass = "w-3.5 h-3.5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind classes shared by the image editor and its panels (same in the React and Svelte
|
|
3
|
+
* packages). They use shadcn theme tokens (`background`, `foreground`, `muted`, `accent`,
|
|
4
|
+
* `border`, `input`, `primary`, `destructive`), so the editor follows the host's light or dark theme.
|
|
5
|
+
*/
|
|
6
|
+
/** The editor's frame. */
|
|
7
|
+
export const imageEditorFrameClass = 'relative flex flex-col bg-background border border-border rounded-xl overflow-hidden shadow-sm text-foreground';
|
|
8
|
+
/** The bar above the canvas (tabs and global actions). */
|
|
9
|
+
export const imageEditorHeaderClass = 'flex flex-wrap items-center justify-between px-4 py-3 bg-muted/40 border-b border-border gap-3';
|
|
10
|
+
/** The tab strip. */
|
|
11
|
+
export const imageTabListClass = 'flex items-center gap-1 bg-background p-1 rounded-lg border border-border text-xs';
|
|
12
|
+
/** A tab of the editor, highlighted when `active`. */
|
|
13
|
+
export const imageTabClass = (active) => `px-3 py-1.5 rounded-md font-medium transition-colors ${active ? 'bg-primary text-primary-foreground shadow' : 'text-muted-foreground hover:text-foreground hover:bg-muted'}`;
|
|
14
|
+
/** A bordered button in the header (undo, redo, zoom…). */
|
|
15
|
+
export const imageHeaderButtonClass = 'p-1.5 rounded-lg border border-border bg-background text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed';
|
|
16
|
+
/** The primary action (export, apply). */
|
|
17
|
+
export const imagePrimaryButtonClass = 'rounded-lg bg-primary hover:bg-primary/90 text-primary-foreground font-medium shadow transition-colors disabled:opacity-50';
|
|
18
|
+
/** The side panel next to the canvas. */
|
|
19
|
+
export const imageSidebarClass = 'w-full md:w-80 bg-background border-t md:border-t-0 md:border-l border-border p-4 overflow-y-auto h-auto md:h-full md:max-h-[560px]';
|
|
20
|
+
/** The canvas area's background. */
|
|
21
|
+
export const imageCanvasAreaClass = 'bg-muted';
|
|
22
|
+
/** A small uppercase section title. */
|
|
23
|
+
export const imageSectionTitleClass = 'font-semibold text-foreground uppercase tracking-wider text-[11px]';
|
|
24
|
+
/** A section under a rule. */
|
|
25
|
+
export const imageSectionClass = 'border-t border-border pt-3';
|
|
26
|
+
/** A thin rule between groups of controls. */
|
|
27
|
+
export const imageDividerClass = 'h-px bg-border my-1';
|
|
28
|
+
/** A small secondary button. */
|
|
29
|
+
export const imageSmallButtonClass = 'px-2 py-1 rounded text-[11px] bg-muted text-foreground hover:bg-accent hover:text-accent-foreground self-start disabled:opacity-40';
|
|
30
|
+
/** A tiny button in a row (layer actions, effect actions). */
|
|
31
|
+
export const imageIconButtonClass = 'px-1.5 py-0.5 rounded text-[10px] bg-muted text-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-40';
|
|
32
|
+
/** An outlined tiny button. */
|
|
33
|
+
export const imageOutlineButtonClass = 'rounded border border-border px-1.5 py-0.5 text-[11px] text-foreground hover:bg-muted disabled:opacity-40';
|
|
34
|
+
/** A toggle chip, filled when `on`. */
|
|
35
|
+
export const imageChipClass = (on) => `px-2 py-1 rounded text-[11px] ${on ? 'bg-primary text-primary-foreground' : 'bg-muted text-foreground hover:bg-accent hover:text-accent-foreground'}`;
|
|
36
|
+
/** A bordered choice button, highlighted when `on`. */
|
|
37
|
+
export const imageChoiceClass = (on) => `border transition-colors ${on ? 'bg-primary/15 border-primary text-foreground' : 'bg-background border-border text-muted-foreground hover:text-foreground hover:bg-muted'}`;
|
|
38
|
+
/** A text or number field. */
|
|
39
|
+
export const imageFieldClass = 'rounded border border-input bg-background px-1 py-0.5 text-foreground';
|
|
40
|
+
/** A select. */
|
|
41
|
+
export const imageSelectClass = 'rounded border border-input bg-background px-1.5 py-1 text-foreground';
|
|
42
|
+
/** A colour input swatch. */
|
|
43
|
+
export const imageColorInputClass = 'h-6 w-8 rounded border border-border bg-transparent';
|
|
44
|
+
/** A full-width range slider. */
|
|
45
|
+
export const imageRangeClass = 'w-full accent-primary';
|
|
46
|
+
/** An error message. */
|
|
47
|
+
export const imageErrorClass = 'text-destructive';
|
|
48
|
+
/** An error banner. */
|
|
49
|
+
export const imageErrorBannerClass = 'border-b border-destructive/40 bg-destructive/10 px-4 py-1.5 text-xs text-destructive';
|
|
50
|
+
/** A spinner on a primary button. */
|
|
51
|
+
export const imageSpinnerClass = 'w-3.5 h-3.5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PresenceStore, type AgentPlayback, type PlaybackSurface } from '@pixerate/editor/tools';
|
|
1
|
+
import { PresenceStore, type AgentPlayback, type PlaybackChromeOptions, type PlaybackSurface } from '@pixerate/editor/tools';
|
|
2
2
|
type MaybeGetter<T> = T | (() => T);
|
|
3
3
|
/** Makes `playback` the default for every editor surface in this component's subtree (call during init). */
|
|
4
4
|
export declare function setAgentPlayback(playback: AgentPlayback | null | undefined): void;
|
|
@@ -15,11 +15,14 @@ export type PlaybackSurfaceOptions = Omit<PlaybackSurface, 'presence'>;
|
|
|
15
15
|
export declare function createPlaybackSurface(playback: MaybeGetter<AgentPlayback | null | undefined>, id: MaybeGetter<string | null | undefined>, options?: MaybeGetter<PlaybackSurfaceOptions>,
|
|
16
16
|
/** Use this presence store (e.g. a director's, so its cursor and playback's are one) instead of a new one. */
|
|
17
17
|
sharedPresence?: PresenceStore | null): PresenceStore;
|
|
18
|
-
export interface ElementPlaybackSurfaceOptions {
|
|
18
|
+
export interface ElementPlaybackSurfaceOptions extends PlaybackChromeOptions {
|
|
19
19
|
/** The kind of a bare `id` / `ids` argument (e.g. `parameter`). */
|
|
20
20
|
defaultKind?: string;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* A CSS selector for each ref kind the surface can find inside the element. Refs they miss go to
|
|
23
|
+
* `locate`, then `locateElement` (which may return an element outside this one).
|
|
24
|
+
*/
|
|
25
|
+
selectors?: Record<string, (id: string) => string>;
|
|
23
26
|
/** Bring the element on screen (switch to its tab) before the agent acts. */
|
|
24
27
|
reveal?: () => void | Promise<void>;
|
|
25
28
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getContext, setContext } from 'svelte';
|
|
2
|
-
import { PresenceStore } from '@pixerate/editor/tools';
|
|
2
|
+
import { PresenceStore, locatePlaybackChrome } from '@pixerate/editor/tools';
|
|
3
3
|
const KEY = Symbol.for('pixerate.agentPlayback');
|
|
4
4
|
const read = (value) => (typeof value === 'function' ? value() : value);
|
|
5
5
|
/** Makes `playback` the default for every editor surface in this component's subtree (call during init). */
|
|
@@ -38,7 +38,11 @@ sharedPresence) {
|
|
|
38
38
|
beginGesture: (key) => o().beginGesture?.(key),
|
|
39
39
|
endGesture: () => o().endGesture?.(),
|
|
40
40
|
reveal: () => o().reveal?.(),
|
|
41
|
-
open
|
|
41
|
+
// Only a surface whose options can open things has `open`, so playback skips `open` steps
|
|
42
|
+
// (and their wait) everywhere else. Read per step, like the other options.
|
|
43
|
+
get open() {
|
|
44
|
+
return o().open ? (ref) => o().open?.(ref) : undefined;
|
|
45
|
+
},
|
|
42
46
|
get defaultKind() {
|
|
43
47
|
return o().defaultKind;
|
|
44
48
|
}
|
|
@@ -59,15 +63,21 @@ sharedPresence) {
|
|
|
59
63
|
*/
|
|
60
64
|
export function createElementPlaybackSurface(playback, id, element, options) {
|
|
61
65
|
const box = () => element()?.getBoundingClientRect() ?? null;
|
|
66
|
+
const fromViewport = (p) => {
|
|
67
|
+
const r = box();
|
|
68
|
+
return { x: p.x - (r?.left ?? 0), y: p.y - (r?.top ?? 0) };
|
|
69
|
+
};
|
|
62
70
|
return createPlaybackSurface(playback, id, () => {
|
|
63
71
|
const o = read(options);
|
|
64
72
|
return {
|
|
65
73
|
defaultKind: o.defaultKind,
|
|
66
74
|
locate: (ref) => {
|
|
67
|
-
const selector = o.selectors[ref.kind]?.(ref.id);
|
|
75
|
+
const selector = o.selectors?.[ref.kind]?.(ref.id);
|
|
68
76
|
const b = box();
|
|
69
77
|
const r = selector ? element()?.querySelector(selector)?.getBoundingClientRect() : null;
|
|
70
|
-
|
|
78
|
+
if (b && r && r.width > 0)
|
|
79
|
+
return { x: r.left - b.left, y: r.top - b.top, width: r.width, height: r.height };
|
|
80
|
+
return locatePlaybackChrome(ref, o, fromViewport);
|
|
71
81
|
},
|
|
72
82
|
bounds: () => {
|
|
73
83
|
const r = box();
|
|
@@ -81,11 +91,9 @@ export function createElementPlaybackSurface(playback, id, element, options) {
|
|
|
81
91
|
const r = box();
|
|
82
92
|
return { x: (r?.left ?? 0) + p.x, y: (r?.top ?? 0) + p.y };
|
|
83
93
|
},
|
|
84
|
-
fromViewport
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
},
|
|
88
|
-
reveal: () => o.reveal?.()
|
|
94
|
+
fromViewport,
|
|
95
|
+
reveal: () => o.reveal?.(),
|
|
96
|
+
open: o.open ? (ref) => o.open?.(ref) : undefined
|
|
89
97
|
};
|
|
90
98
|
});
|
|
91
99
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.109.0",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"@tiptap/extension-bubble-menu": "^2.27.3",
|
|
161
161
|
"@tiptap/extension-placeholder": "^2.27.3",
|
|
162
162
|
"@tiptap/starter-kit": "^2.27.3",
|
|
163
|
-
"@pixerate/editor": "0.
|
|
163
|
+
"@pixerate/editor": "0.149.0"
|
|
164
164
|
},
|
|
165
165
|
"peerDependencies": {
|
|
166
166
|
"@dagrejs/dagre": "^1.1.4",
|