@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,296 @@
1
+ <script lang="ts">
2
+ import ColorPicker from './ColorPicker.svelte';
3
+ import {
4
+ type ColorScheme,
5
+ type ColorValue,
6
+ type DualColor,
7
+ DEFAULT_SCHEME_SURFACES,
8
+ normalizeDualColor,
9
+ resolveColor,
10
+ setDualColorSide,
11
+ deriveDualColor,
12
+ checkDualColorContrast,
13
+ isSameInBothSchemes
14
+ } from '@pixerate/editor/color';
15
+
16
+ interface Props {
17
+ /** Dual colour value (CSS string, light-dark(), or { light, dark }) */
18
+ value?: ColorValue;
19
+ /** Called when the dual colour value changes */
20
+ onchange?: (value: DualColor) => void;
21
+ onChange?: (value: DualColor) => void;
22
+ /** Controlled active scheme mode ('light' | 'dark') */
23
+ scheme?: ColorScheme;
24
+ /** Called when the user toggles active scheme tab */
25
+ onschemechange?: (scheme: ColorScheme) => void;
26
+ onSchemeChange?: (scheme: ColorScheme) => void;
27
+ /** Expected background surfaces for derivation and preview */
28
+ surfaces?: DualColor;
29
+ /** Optional background colour to evaluate contrast against in both modes */
30
+ contrastAgainst?: ColorValue;
31
+ /** Whether alpha slider is shown */
32
+ alpha?: boolean;
33
+ /** Preset swatches */
34
+ swatches?: readonly string[];
35
+ /** Whether picker is disabled */
36
+ disabled?: boolean;
37
+ /** Additional CSS class name */
38
+ class?: string;
39
+ className?: string;
40
+ /** Gesture callbacks */
41
+ ongesturestart?: () => void;
42
+ onGestureStart?: () => void;
43
+ ongestureend?: () => void;
44
+ onGestureEnd?: () => void;
45
+ }
46
+
47
+ let {
48
+ value = DEFAULT_SCHEME_SURFACES,
49
+ onchange,
50
+ onChange,
51
+ scheme: controlledScheme,
52
+ onschemechange,
53
+ onSchemeChange,
54
+ surfaces = DEFAULT_SCHEME_SURFACES,
55
+ contrastAgainst,
56
+ alpha = false,
57
+ swatches,
58
+ disabled = false,
59
+ class: classProp,
60
+ className: classNameProp,
61
+ ongesturestart,
62
+ onGestureStart,
63
+ ongestureend,
64
+ onGestureEnd
65
+ }: Props = $props();
66
+
67
+ const className = $derived(classProp || classNameProp || '');
68
+
69
+ const triggerChange = (val: DualColor) => {
70
+ onchange?.(val);
71
+ onChange?.(val);
72
+ };
73
+
74
+ const triggerSchemeChange = (s: ColorScheme) => {
75
+ onschemechange?.(s);
76
+ onSchemeChange?.(s);
77
+ };
78
+
79
+ const triggerGestureStart = () => {
80
+ ongesturestart?.();
81
+ onGestureStart?.();
82
+ };
83
+
84
+ const triggerGestureEnd = () => {
85
+ ongestureend?.();
86
+ onGestureEnd?.();
87
+ };
88
+
89
+ // Internal active scheme state
90
+ let internalScheme = $state<ColorScheme>('light');
91
+ const activeScheme = $derived(controlledScheme ?? internalScheme);
92
+
93
+ // Normalised dual value
94
+ const dual = $derived<DualColor>(
95
+ normalizeDualColor(value) ?? DEFAULT_SCHEME_SURFACES
96
+ );
97
+
98
+ // Same in both mode state
99
+ let sameInBoth = $state<boolean>(false);
100
+ let initializedSame = false;
101
+
102
+ $effect(() => {
103
+ if (!initializedSame) {
104
+ sameInBoth = isSameInBothSchemes(dual);
105
+ initializedSame = true;
106
+ } else if (isSameInBothSchemes(dual)) {
107
+ sameInBoth = true;
108
+ }
109
+ });
110
+
111
+ const handleSchemeSelect = (nextScheme: ColorScheme) => {
112
+ if (disabled) return;
113
+ internalScheme = nextScheme;
114
+ triggerSchemeChange(nextScheme);
115
+ };
116
+
117
+ const handleToggleSameInBoth = (checked: boolean) => {
118
+ if (disabled) return;
119
+ sameInBoth = checked;
120
+ if (checked) {
121
+ const activeColor = activeScheme === 'dark' ? dual.dark : dual.light;
122
+ triggerChange({ light: activeColor, dark: activeColor });
123
+ }
124
+ };
125
+
126
+ const handleDerive = () => {
127
+ if (disabled) return;
128
+ triggerGestureStart();
129
+ const currentActive = activeScheme === 'dark' ? dual.dark : dual.light;
130
+ const derived = deriveDualColor(currentActive, {
131
+ from: activeScheme,
132
+ // An unreadable surfaces prop falls back to the defaults rather than throwing.
133
+ surfaces: normalizeDualColor(surfaces) ?? DEFAULT_SCHEME_SURFACES
134
+ });
135
+ triggerChange(derived);
136
+ triggerGestureEnd();
137
+ };
138
+
139
+ const handleInnerChange = (hex: string) => {
140
+ if (sameInBoth) {
141
+ triggerChange({ light: hex, dark: hex });
142
+ } else {
143
+ const updated = setDualColorSide(dual, activeScheme, hex);
144
+ triggerChange(updated);
145
+ }
146
+ };
147
+
148
+ const activeColor = $derived(activeScheme === 'dark' ? dual.dark : dual.light);
149
+ const activeContrastTarget = $derived(
150
+ contrastAgainst ? resolveColor(contrastAgainst, activeScheme) ?? undefined : undefined
151
+ );
152
+
153
+ const dualContrast = $derived(
154
+ // An unreadable contrastAgainst shows no badges rather than throwing during render.
155
+ contrastAgainst && normalizeDualColor(contrastAgainst)
156
+ ? checkDualColorContrast(dual, contrastAgainst)
157
+ : null
158
+ );
159
+
160
+ const CHECKERBOARD_BG =
161
+ 'linear-gradient(45deg, #2a2e39 25%, transparent 25%), linear-gradient(-45deg, #2a2e39 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #2a2e39 75%), linear-gradient(-45deg, transparent 75%, #2a2e39 75%)';
162
+ </script>
163
+
164
+ <div
165
+ class="pixerate-dual-color-picker {className}"
166
+ style="display: flex; flex-direction: column; gap: 12px; padding: 12px; background: #1a1d24; color: #e2e8f0; border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); width: 260px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; box-sizing: border-box;"
167
+ >
168
+ <!-- Segmented Control -->
169
+ <div
170
+ role="tablist"
171
+ aria-label="Color scheme mode"
172
+ style="display: flex; background: #111318; border-radius: 6px; padding: 2px; gap: 2px;"
173
+ >
174
+ <button
175
+ type="button"
176
+ role="tab"
177
+ aria-selected={activeScheme === 'light'}
178
+ onclick={() => handleSchemeSelect('light')}
179
+ {disabled}
180
+ style="flex: 1; padding: 6px 8px; border: none; border-radius: 4px; background: {activeScheme === 'light' ? '#2d3342' : 'transparent'}; color: {activeScheme === 'light' ? '#ffffff' : '#8c95a6'}; font-weight: {activeScheme === 'light' ? 600 : 400}; cursor: {disabled ? 'not-allowed' : 'pointer'}; font-size: 12px; transition: all 0.15s ease;"
181
+ >
182
+ Light
183
+ </button>
184
+ <button
185
+ type="button"
186
+ role="tab"
187
+ aria-selected={activeScheme === 'dark'}
188
+ onclick={() => handleSchemeSelect('dark')}
189
+ {disabled}
190
+ style="flex: 1; padding: 6px 8px; border: none; border-radius: 4px; background: {activeScheme === 'dark' ? '#2d3342' : 'transparent'}; color: {activeScheme === 'dark' ? '#ffffff' : '#8c95a6'}; font-weight: {activeScheme === 'dark' ? 600 : 400}; cursor: {disabled ? 'not-allowed' : 'pointer'}; font-size: 12px; transition: all 0.15s ease;"
191
+ >
192
+ Dark
193
+ </button>
194
+ </div>
195
+
196
+ <!-- Dual Preview Swatches -->
197
+ <div style="display: flex; gap: 8px;">
198
+ <!-- Light preview button -->
199
+ <button
200
+ type="button"
201
+ onclick={() => handleSchemeSelect('light')}
202
+ {disabled}
203
+ title="Light scheme colour preview"
204
+ style="flex: 1; display: flex; flex-direction: column; gap: 4px; padding: 6px; background: {activeScheme === 'light' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.2)'}; border: 1px solid {activeScheme === 'light' ? '#4f46e5' : 'rgba(255,255,255,0.08)'}; border-radius: 6px; cursor: {disabled ? 'not-allowed' : 'pointer'}; text-align: left;"
205
+ >
206
+ <div style="display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: #8c95a6;">
207
+ <span>Light</span>
208
+ <span style="font-family: monospace; font-size: 10px;">{dual.light}</span>
209
+ </div>
210
+ <div
211
+ style="height: 24px; border-radius: 4px; background-image: {CHECKERBOARD_BG}; background-size: 8px 8px; overflow: hidden; border: 1px solid rgba(0,0,0,0.15);"
212
+ >
213
+ <div style="width: 100%; height: 100%; background: {dual.light};"></div>
214
+ </div>
215
+ </button>
216
+
217
+ <!-- Dark preview button -->
218
+ <button
219
+ type="button"
220
+ onclick={() => handleSchemeSelect('dark')}
221
+ {disabled}
222
+ title="Dark scheme colour preview"
223
+ style="flex: 1; display: flex; flex-direction: column; gap: 4px; padding: 6px; background: {activeScheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.2)'}; border: 1px solid {activeScheme === 'dark' ? '#4f46e5' : 'rgba(255,255,255,0.08)'}; border-radius: 6px; cursor: {disabled ? 'not-allowed' : 'pointer'}; text-align: left;"
224
+ >
225
+ <div style="display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: #8c95a6;">
226
+ <span>Dark</span>
227
+ <span style="font-family: monospace; font-size: 10px;">{dual.dark}</span>
228
+ </div>
229
+ <div
230
+ style="height: 24px; border-radius: 4px; background-image: {CHECKERBOARD_BG}; background-size: 8px 8px; overflow: hidden; border: 1px solid rgba(0,0,0,0.15);"
231
+ >
232
+ <div style="width: 100%; height: 100%; background: {dual.dark};"></div>
233
+ </div>
234
+ </button>
235
+ </div>
236
+
237
+ <!-- Dual Toolbar: Same in both toggle and Derive button -->
238
+ <div style="display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: #cbd5e1;">
239
+ <label style="display: flex; align-items: center; gap: 6px; cursor: {disabled ? 'not-allowed' : 'pointer'}; user-select: none;">
240
+ <input
241
+ type="checkbox"
242
+ checked={sameInBoth}
243
+ onchange={(e) => handleToggleSameInBoth(e.currentTarget.checked)}
244
+ {disabled}
245
+ style="margin: 0; cursor: {disabled ? 'not-allowed' : 'pointer'};"
246
+ />
247
+ Same in both
248
+ </label>
249
+
250
+ <button
251
+ type="button"
252
+ onclick={handleDerive}
253
+ {disabled}
254
+ title="Automatically derive the opposing scheme colour preserving contrast"
255
+ style="padding: 2px 8px; font-size: 11px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); border-radius: 4px; color: #cbd5e1; cursor: {disabled ? 'not-allowed' : 'pointer'};"
256
+ >
257
+ Derive {activeScheme === 'light' ? 'Dark' : 'Light'}
258
+ </button>
259
+ </div>
260
+
261
+ <!-- Embedded Headless ColorPicker for the active scheme side -->
262
+ <ColorPicker
263
+ value={activeColor}
264
+ onchange={handleInnerChange}
265
+ contrastAgainst={activeContrastTarget}
266
+ {alpha}
267
+ {swatches}
268
+ {disabled}
269
+ ongesturestart={triggerGestureStart}
270
+ ongestureend={triggerGestureEnd}
271
+ />
272
+
273
+ <!-- Optional Dual Contrast Badge Summary -->
274
+ {#if dualContrast}
275
+ <div
276
+ style="display: flex; gap: 8px; padding-top: 4px; border-top: 1px solid rgba(255,255,255,0.08); font-size: 11px;"
277
+ >
278
+ <div style="flex: 1; display: flex; justify-content: space-between; align-items: center;">
279
+ <span style="color: #8c95a6;">Light:</span>
280
+ <span
281
+ style="padding: 1px 4px; border-radius: 3px; font-weight: 600; font-family: monospace; background: {dualContrast.light.level === 'fail' ? '#7f1d1d' : '#14532d'}; color: {dualContrast.light.level === 'fail' ? '#fca5a5' : '#86efac'};"
282
+ >
283
+ {dualContrast.light.ratio}:1 ({dualContrast.light.level.toUpperCase()})
284
+ </span>
285
+ </div>
286
+ <div style="flex: 1; display: flex; justify-content: space-between; align-items: center;">
287
+ <span style="color: #8c95a6;">Dark:</span>
288
+ <span
289
+ style="padding: 1px 4px; border-radius: 3px; font-weight: 600; font-family: monospace; background: {dualContrast.dark.level === 'fail' ? '#7f1d1d' : '#14532d'}; color: {dualContrast.dark.level === 'fail' ? '#fca5a5' : '#86efac'};"
290
+ >
291
+ {dualContrast.dark.ratio}:1 ({dualContrast.dark.level.toUpperCase()})
292
+ </span>
293
+ </div>
294
+ </div>
295
+ {/if}
296
+ </div>
@@ -0,0 +1,34 @@
1
+ import { type ColorScheme, type ColorValue, type DualColor } from '@pixerate/editor/color';
2
+ interface Props {
3
+ /** Dual colour value (CSS string, light-dark(), or { light, dark }) */
4
+ value?: ColorValue;
5
+ /** Called when the dual colour value changes */
6
+ onchange?: (value: DualColor) => void;
7
+ onChange?: (value: DualColor) => void;
8
+ /** Controlled active scheme mode ('light' | 'dark') */
9
+ scheme?: ColorScheme;
10
+ /** Called when the user toggles active scheme tab */
11
+ onschemechange?: (scheme: ColorScheme) => void;
12
+ onSchemeChange?: (scheme: ColorScheme) => void;
13
+ /** Expected background surfaces for derivation and preview */
14
+ surfaces?: DualColor;
15
+ /** Optional background colour to evaluate contrast against in both modes */
16
+ contrastAgainst?: ColorValue;
17
+ /** Whether alpha slider is shown */
18
+ alpha?: boolean;
19
+ /** Preset swatches */
20
+ swatches?: readonly string[];
21
+ /** Whether picker is disabled */
22
+ disabled?: boolean;
23
+ /** Additional CSS class name */
24
+ class?: string;
25
+ className?: string;
26
+ /** Gesture callbacks */
27
+ ongesturestart?: () => void;
28
+ onGestureStart?: () => void;
29
+ ongestureend?: () => void;
30
+ onGestureEnd?: () => void;
31
+ }
32
+ declare const DualColorPicker: import("svelte").Component<Props, {}, "">;
33
+ type DualColorPicker = ReturnType<typeof DualColorPicker>;
34
+ export default DualColorPicker;
@@ -0,0 +1,19 @@
1
+ import type { ColorScheme, ColorValue } from '@pixerate/editor/color';
2
+ export interface ColorSchemeOptions {
3
+ /** Explicit scheme override ('light' or 'dark') */
4
+ override?: ColorScheme | null;
5
+ /** Attribute on document.documentElement to monitor for theme changes (default: 'data-theme') */
6
+ attribute?: string;
7
+ }
8
+ export interface ColorSchemeStore {
9
+ readonly current: ColorScheme;
10
+ destroy(): void;
11
+ }
12
+ /**
13
+ * Headless Svelte 5 rune tracking active colour scheme across media query and document attribute.
14
+ */
15
+ export declare function createColorScheme(options?: ColorSchemeOptions): ColorSchemeStore;
16
+ /**
17
+ * Pure helper resolving a ColorValue against a specific scheme.
18
+ */
19
+ export declare function resolvedColor(value: ColorValue, scheme: ColorScheme): string | null;
@@ -0,0 +1,76 @@
1
+ import { resolveColor } from '@pixerate/editor/color';
2
+ /**
3
+ * Headless Svelte 5 rune tracking active colour scheme across media query and document attribute.
4
+ */
5
+ export function createColorScheme(options = {}) {
6
+ let system = $state('light');
7
+ let attrValue = $state(null);
8
+ const attributeName = options.attribute ?? 'data-theme';
9
+ function readSystem() {
10
+ if (typeof window === 'undefined' || !window.matchMedia)
11
+ return 'light';
12
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
13
+ }
14
+ function readAttribute() {
15
+ if (typeof document === 'undefined')
16
+ return null;
17
+ const val = document.documentElement.getAttribute(attributeName)?.toLowerCase();
18
+ if (val === 'dark' || val === 'light')
19
+ return val;
20
+ return null;
21
+ }
22
+ system = readSystem();
23
+ attrValue = readAttribute();
24
+ let mqlListener = null;
25
+ let observer = null;
26
+ if (typeof window !== 'undefined' && window.matchMedia) {
27
+ const mql = window.matchMedia('(prefers-color-scheme: dark)');
28
+ mqlListener = (e) => {
29
+ system = e.matches ? 'dark' : 'light';
30
+ };
31
+ if (mql.addEventListener) {
32
+ mql.addEventListener('change', mqlListener);
33
+ }
34
+ else {
35
+ mql.addListener(mqlListener);
36
+ }
37
+ }
38
+ if (typeof document !== 'undefined' && typeof MutationObserver !== 'undefined') {
39
+ observer = new MutationObserver(() => {
40
+ attrValue = readAttribute();
41
+ });
42
+ observer.observe(document.documentElement, {
43
+ attributes: true,
44
+ attributeFilter: [attributeName]
45
+ });
46
+ }
47
+ function destroy() {
48
+ if (mqlListener && typeof window !== 'undefined' && window.matchMedia) {
49
+ const mql = window.matchMedia('(prefers-color-scheme: dark)');
50
+ if (mql.removeEventListener) {
51
+ mql.removeEventListener('change', mqlListener);
52
+ }
53
+ else {
54
+ mql.removeListener(mqlListener);
55
+ }
56
+ mqlListener = null;
57
+ }
58
+ if (observer) {
59
+ observer.disconnect();
60
+ observer = null;
61
+ }
62
+ }
63
+ const current = $derived(options.override ?? attrValue ?? system);
64
+ return {
65
+ get current() {
66
+ return current;
67
+ },
68
+ destroy
69
+ };
70
+ }
71
+ /**
72
+ * Pure helper resolving a ColorValue against a specific scheme.
73
+ */
74
+ export function resolvedColor(value, scheme) {
75
+ return resolveColor(value, scheme);
76
+ }
@@ -0,0 +1,9 @@
1
+ import type { ComponentProps } from 'svelte';
2
+ import type ColorPickerComponent from './ColorPicker.svelte';
3
+ import type DualColorPickerComponent from './DualColorPicker.svelte';
4
+ export { default as ColorPicker } from './ColorPicker.svelte';
5
+ export { default as DualColorPicker } from './DualColorPicker.svelte';
6
+ export type ColorPickerProps = ComponentProps<typeof ColorPickerComponent>;
7
+ export type DualColorPickerProps = ComponentProps<typeof DualColorPickerComponent>;
8
+ export { createColorScheme, resolvedColor, type ColorSchemeOptions, type ColorSchemeStore } from './colorScheme.svelte.js';
9
+ export * from '@pixerate/editor/color';
@@ -0,0 +1,5 @@
1
+ export { default as ColorPicker } from './ColorPicker.svelte';
2
+ export { default as DualColorPicker } from './DualColorPicker.svelte';
3
+ export { createColorScheme, resolvedColor } from './colorScheme.svelte.js';
4
+ // Re-export core colour module for consumer convenience
5
+ export * from '@pixerate/editor/color';