@pixerate/editor-svelte 0.105.0 → 0.107.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.
@@ -0,0 +1,236 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import type { FillMode } from './types.js';
4
+ import {
5
+ GradientController,
6
+ createGradient,
7
+ gradientToCss,
8
+ parseCssGradient
9
+ } from '@pixerate/editor/gradient';
10
+ import { formatHex, mixColors, parseColor, resolveColor } from '@pixerate/editor/color';
11
+ import ColorPicker from '../color/ColorPicker.svelte';
12
+ import GradientEditor from './GradientEditor.svelte';
13
+
14
+ interface Props {
15
+ /** Active CSS colour (hex/rgb/hsl etc.) or CSS gradient string. */
16
+ value: string;
17
+ /** Called when the fill value changes. */
18
+ onchange?: (value: string) => void;
19
+ onChange?: (value: string) => void;
20
+ /** Called when a continuous pointer drag starts. */
21
+ ongesturestart?: () => void;
22
+ onGestureStart?: () => void;
23
+ /** Called when a continuous pointer drag ends. */
24
+ ongestureend?: () => void;
25
+ onGestureEnd?: () => void;
26
+ /** Allowed fill modes. Default: ['solid', 'gradient']. */
27
+ modes?: readonly FillMode[];
28
+ /** Whether the alpha channel slider is shown on ColorPicker. Default: false. */
29
+ alpha?: boolean;
30
+ /** Preset swatches for ColorPicker. */
31
+ swatches?: readonly string[];
32
+ /** Whether the gradient editor shows preset buttons. Default: true. */
33
+ showPresets?: boolean;
34
+ /** Whether the gradient editor shows the CSS output with a copy button. Default: false. */
35
+ showCss?: boolean;
36
+ /** Additional CSS class names. */
37
+ class?: string;
38
+ className?: string;
39
+ /** Whether the picker is disabled. */
40
+ disabled?: boolean;
41
+ }
42
+
43
+ let {
44
+ value,
45
+ onchange,
46
+ onChange,
47
+ ongesturestart,
48
+ onGestureStart,
49
+ ongestureend,
50
+ onGestureEnd,
51
+ modes = ['solid', 'gradient'],
52
+ alpha = false,
53
+ swatches,
54
+ showPresets = true,
55
+ showCss = false,
56
+ class: klass = '',
57
+ className = '',
58
+ disabled = false
59
+ }: Props = $props();
60
+
61
+ function emitChange(v: string) {
62
+ onchange?.(v);
63
+ onChange?.(v);
64
+ }
65
+
66
+ function emitGestureStart() {
67
+ ongesturestart?.();
68
+ onGestureStart?.();
69
+ }
70
+
71
+ function emitGestureEnd() {
72
+ ongestureend?.();
73
+ onGestureEnd?.();
74
+ }
75
+
76
+ function createDefaultGradientFromColor(colorStr: string) {
77
+ const parsed = parseColor(colorStr);
78
+ const baseHex = parsed ? formatHex(parsed) : '#3b82f6';
79
+ const lighterHex = mixColors(baseHex, '#ffffff', 0.35, { space: 'oklab' }) ?? '#ffffff';
80
+ return createGradient([baseHex, lighterHex]);
81
+ }
82
+
83
+ // The controller and mode start from the first value; the effect below follows later ones.
84
+ const initialValue = untrack(() => value);
85
+ const initialGradient = parseCssGradient(initialValue);
86
+ const allows = (m: FillMode) => untrack(() => modes).includes(m);
87
+
88
+ let mode = $state<FillMode>(
89
+ initialGradient && allows('gradient') ? 'gradient' : allows('solid') ? 'solid' : 'gradient'
90
+ );
91
+ let isDragging = false;
92
+ // The last CSS this picker emitted, so the echo of our own change isn't loaded back in.
93
+ let emitted: string | null = null;
94
+ let loading = false;
95
+
96
+ const controller = new GradientController({
97
+ value: initialGradient ?? createDefaultGradientFromColor(initialValue),
98
+ readOnly: () => disabled,
99
+ onChange: (g) => {
100
+ if (loading || mode !== 'gradient') return;
101
+ emit(gradientToCss(g));
102
+ }
103
+ });
104
+
105
+ function emit(v: string) {
106
+ emitted = v;
107
+ emitChange(v);
108
+ }
109
+
110
+ function loadQuietly(next: unknown) {
111
+ loading = true;
112
+ try {
113
+ controller.load(next);
114
+ } finally {
115
+ loading = false;
116
+ }
117
+ }
118
+
119
+ // Follow the incoming value, without an undo step, unless it is our own change coming back.
120
+ $effect(() => {
121
+ const incoming = parseCssGradient(value);
122
+ const allowsGradient = modes.includes('gradient');
123
+ const allowsSolid = modes.includes('solid');
124
+ untrack(() => {
125
+ if (isDragging) return;
126
+ if (incoming && allowsGradient) {
127
+ mode = 'gradient';
128
+ if (value !== emitted) loadQuietly(incoming);
129
+ } else if (!incoming && allowsSolid) {
130
+ mode = 'solid';
131
+ }
132
+ });
133
+ });
134
+
135
+ function switchToSolid() {
136
+ if (mode === 'solid') return;
137
+ mode = 'solid';
138
+ const first = controller.getGradient().stops[0]?.color ?? '#000000';
139
+ // A dual stop has no single solid equivalent; keep its light half.
140
+ emit(resolveColor(first, 'light') ?? '#000000');
141
+ }
142
+
143
+ function switchToGradient() {
144
+ if (mode === 'gradient') return;
145
+ mode = 'gradient';
146
+ loadQuietly(createDefaultGradientFromColor(value));
147
+ emit(gradientToCss(controller.getGradient()));
148
+ }
149
+
150
+ function gradientGestureStart() {
151
+ isDragging = true;
152
+ emitGestureStart();
153
+ }
154
+
155
+ function gradientGestureEnd() {
156
+ isDragging = false;
157
+ emitGestureEnd();
158
+ }
159
+
160
+ const buttonClass =
161
+ 'inline-flex h-7 items-center justify-center rounded border border-border bg-background px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40 aria-pressed:bg-primary aria-pressed:text-primary-foreground';
162
+ </script>
163
+
164
+ {#if modes.length === 1 && modes[0] === 'solid'}
165
+ <div class="{klass} {className}" data-testid="fill-picker">
166
+ <ColorPicker
167
+ {value}
168
+ onchange={emitChange}
169
+ ongesturestart={emitGestureStart}
170
+ ongestureend={emitGestureEnd}
171
+ {alpha}
172
+ {swatches}
173
+ {disabled}
174
+ />
175
+ </div>
176
+ {:else if modes.length === 1 && modes[0] === 'gradient'}
177
+ <div class="{klass} {className}" data-testid="fill-picker">
178
+ <GradientEditor
179
+ {controller}
180
+ {showPresets}
181
+ {showCss}
182
+ ongesturestart={gradientGestureStart}
183
+ ongestureend={gradientGestureEnd}
184
+ />
185
+ </div>
186
+ {:else}
187
+ <div class="flex flex-col gap-2.5 {klass} {className}" data-testid="fill-picker">
188
+ <!-- Mode toggle tabs -->
189
+ <div class="flex rounded-md border border-border p-0.5">
190
+ <button
191
+ type="button"
192
+ class="flex-1 {buttonClass}"
193
+ aria-pressed={mode === 'solid'}
194
+ {disabled}
195
+ onclick={switchToSolid}
196
+ >
197
+ Solid
198
+ </button>
199
+ <button
200
+ type="button"
201
+ class="flex-1 {buttonClass}"
202
+ aria-pressed={mode === 'gradient'}
203
+ {disabled}
204
+ onclick={switchToGradient}
205
+ >
206
+ Gradient
207
+ </button>
208
+ </div>
209
+
210
+ {#if mode === 'solid'}
211
+ <ColorPicker
212
+ {value}
213
+ onchange={emitChange}
214
+ ongesturestart={() => {
215
+ isDragging = true;
216
+ emitGestureStart();
217
+ }}
218
+ ongestureend={() => {
219
+ isDragging = false;
220
+ emitGestureEnd();
221
+ }}
222
+ {alpha}
223
+ {swatches}
224
+ {disabled}
225
+ />
226
+ {:else}
227
+ <GradientEditor
228
+ {controller}
229
+ {showPresets}
230
+ {showCss}
231
+ ongesturestart={gradientGestureStart}
232
+ ongestureend={gradientGestureEnd}
233
+ />
234
+ {/if}
235
+ </div>
236
+ {/if}
@@ -0,0 +1,32 @@
1
+ import type { FillMode } from './types.js';
2
+ interface Props {
3
+ /** Active CSS colour (hex/rgb/hsl etc.) or CSS gradient string. */
4
+ value: string;
5
+ /** Called when the fill value changes. */
6
+ onchange?: (value: string) => void;
7
+ onChange?: (value: string) => void;
8
+ /** Called when a continuous pointer drag starts. */
9
+ ongesturestart?: () => void;
10
+ onGestureStart?: () => void;
11
+ /** Called when a continuous pointer drag ends. */
12
+ ongestureend?: () => void;
13
+ onGestureEnd?: () => void;
14
+ /** Allowed fill modes. Default: ['solid', 'gradient']. */
15
+ modes?: readonly FillMode[];
16
+ /** Whether the alpha channel slider is shown on ColorPicker. Default: false. */
17
+ alpha?: boolean;
18
+ /** Preset swatches for ColorPicker. */
19
+ swatches?: readonly string[];
20
+ /** Whether the gradient editor shows preset buttons. Default: true. */
21
+ showPresets?: boolean;
22
+ /** Whether the gradient editor shows the CSS output with a copy button. Default: false. */
23
+ showCss?: boolean;
24
+ /** Additional CSS class names. */
25
+ class?: string;
26
+ className?: string;
27
+ /** Whether the picker is disabled. */
28
+ disabled?: boolean;
29
+ }
30
+ declare const FillPicker: import("svelte").Component<Props, {}, "">;
31
+ type FillPicker = ReturnType<typeof FillPicker>;
32
+ export default FillPicker;