@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.
Files changed (27) hide show
  1. package/dist/canvas/createCanvasPlaybackSurface.svelte.d.ts +8 -2
  2. package/dist/canvas/createCanvasPlaybackSurface.svelte.js +13 -8
  3. package/dist/image-editor/AdjustmentLayersPanel.svelte +27 -26
  4. package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts +2 -0
  5. package/dist/image-editor/BrushOptionsBar.svelte +35 -34
  6. package/dist/image-editor/ColorProfilePanel.svelte +12 -12
  7. package/dist/image-editor/ControlSlider.svelte +6 -4
  8. package/dist/image-editor/ControlSlider.svelte.d.ts +2 -0
  9. package/dist/image-editor/CurvesEditor.svelte +6 -4
  10. package/dist/image-editor/CurvesEditor.svelte.d.ts +2 -0
  11. package/dist/image-editor/FreeTransformOptionsBar.svelte +22 -21
  12. package/dist/image-editor/Histogram.svelte +1 -1
  13. package/dist/image-editor/ImageEditor.svelte +127 -142
  14. package/dist/image-editor/ImageEffectControls.svelte +15 -11
  15. package/dist/image-editor/ImageEffectControls.svelte.d.ts +2 -0
  16. package/dist/image-editor/ImageProviderEffectsPanel.svelte +15 -15
  17. package/dist/image-editor/LayerEffectsPanel.svelte +22 -23
  18. package/dist/image-editor/LayersPanel.svelte +39 -39
  19. package/dist/image-editor/RefineEdgePanel.svelte +14 -13
  20. package/dist/image-editor/SelectionOptionsBar.svelte +12 -11
  21. package/dist/image-editor/ShapeOptionsBar.svelte +14 -13
  22. package/dist/image-editor/TypeOptionsBar.svelte +12 -11
  23. package/dist/image-editor/styles.d.ts +51 -0
  24. package/dist/image-editor/styles.js +51 -0
  25. package/dist/playback/playback.svelte.d.ts +7 -4
  26. package/dist/playback/playback.svelte.js +17 -9
  27. package/package.json +2 -2
@@ -1,7 +1,13 @@
1
1
  import { type CanvasEdge, type CanvasNode, type NodeRectOptions } from '@pixerate/editor/canvas';
2
- import type { AgentPlayback, PresenceStore } from '@pixerate/editor/tools';
2
+ import { type AgentPlayback, type PlaybackChromeOptions, type PresenceStore } from '@pixerate/editor/tools';
3
3
  type MaybeGetter<T> = T | (() => T);
4
- export interface CreateCanvasPlaybackSurfaceOptions<TNode extends CanvasNode = CanvasNode, TEdge extends CanvasEdge = CanvasEdge> {
4
+ /**
5
+ * `locate`, `locateElement` and `open` (see `PlaybackChromeOptions`) let the host's own chrome over
6
+ * the canvas (a HUD, toolbar or panels) answer refs the canvas does not find, such as
7
+ * `{ kind: 'panel' | 'control', id }`: `locate` in flow coordinates, `locateElement` as an element
8
+ * whose client box is mapped into flow coordinates through the viewport.
9
+ */
10
+ export interface CreateCanvasPlaybackSurfaceOptions<TNode extends CanvasNode = CanvasNode, TEdge extends CanvasEdge = CanvasEdge> extends PlaybackChromeOptions {
5
11
  /** The flow's nodes and edges now (e.g. the `bind:nodes` state). */
6
12
  getNodes: () => readonly TNode[];
7
13
  getEdges?: () => readonly TEdge[];
@@ -1,4 +1,5 @@
1
1
  import { createCanvasPlaybackLocator } from '@pixerate/editor/canvas';
2
+ import { locatePlaybackChrome } from '@pixerate/editor/tools';
2
3
  import { createPlaybackSurface } from '../playback/playback.svelte.js';
3
4
  /**
4
5
  * Svelte 5 rune: attaches the node canvas as an agent playback surface under `id` (the canvas tool
@@ -7,15 +8,20 @@ import { createPlaybackSurface } from '../playback/playback.svelte.js';
7
8
  * React's `useCanvasPlaybackSurface`.
8
9
  */
9
10
  export function createCanvasPlaybackSurface(playback, id, options) {
10
- const locate = createCanvasPlaybackLocator({
11
+ const locateFlow = createCanvasPlaybackLocator({
11
12
  getNodes: options.getNodes,
12
13
  getEdges: () => options.getEdges?.() ?? [],
13
14
  rectOptions: options.rectOptions
14
15
  });
15
16
  const box = () => options.container()?.getBoundingClientRect() ?? null;
17
+ const fromViewport = (p) => {
18
+ const r = box();
19
+ const { x, y, zoom } = options.getViewport();
20
+ return { x: (p.x - (r?.left ?? 0) - x) / zoom, y: (p.y - (r?.top ?? 0) - y) / zoom };
21
+ };
16
22
  return createPlaybackSurface(playback, id, {
17
23
  defaultKind: 'node',
18
- locate,
24
+ locate: (ref) => locateFlow(ref) ?? locatePlaybackChrome(ref, options, fromViewport),
19
25
  bounds: () => {
20
26
  const r = box();
21
27
  const { x, y, zoom } = options.getViewport();
@@ -33,11 +39,10 @@ export function createCanvasPlaybackSurface(playback, id, options) {
33
39
  const { x, y, zoom } = options.getViewport();
34
40
  return { x: (r?.left ?? 0) + p.x * zoom + x, y: (r?.top ?? 0) + p.y * zoom + y };
35
41
  },
36
- fromViewport: (p) => {
37
- const r = box();
38
- const { x, y, zoom } = options.getViewport();
39
- return { x: (p.x - (r?.left ?? 0) - x) / zoom, y: (p.y - (r?.top ?? 0) - y) / zoom };
40
- },
41
- reveal: () => options.reveal?.()
42
+ fromViewport,
43
+ reveal: () => options.reveal?.(),
44
+ get open() {
45
+ return options.open ? (ref) => options.open?.(ref) : undefined;
46
+ }
42
47
  });
43
48
  }
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { imageSectionTitleClass, imageSelectClass } from './styles.js';
2
3
  import type {
3
4
  AdjustmentKind,
4
5
  AdjustmentLayer,
@@ -28,9 +29,11 @@
28
29
  /** The current layers (`state.adjustments.layers`), bottom to top. Omit to follow the controller. */
29
30
  layers?: AdjustmentLayer[];
30
31
  histogram?: ImageHistogram | null;
32
+ /** Extra classes on the root element. */
33
+ class?: string;
31
34
  }
32
35
 
33
- let { controller, layers: layersProp, histogram = null }: Props = $props();
36
+ let { controller, layers: layersProp, histogram = null, class: className = '' }: Props = $props();
34
37
 
35
38
  // Follows the controller when no `layers` prop is given.
36
39
  let tick = $state(0);
@@ -66,10 +69,8 @@
66
69
 
67
70
  const selected = $derived(layers.find((l) => l.id === selectedId) ?? null);
68
71
  const topFirst = $derived([...layers].reverse());
69
-
70
- const selectClass = 'bg-slate-800 border border-slate-700 rounded px-1.5 py-1 text-slate-200';
71
72
  const chipClass = (on: boolean) =>
72
- `px-2 py-0.5 rounded text-[10px] ${on ? 'bg-indigo-600 text-white' : 'bg-slate-800 text-slate-400 hover:text-slate-200'}`;
73
+ `px-2 py-0.5 rounded text-[10px] ${on ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground hover:text-foreground'}`;
73
74
 
74
75
  function setParam(layer: AdjustmentLayer, path: string, value: unknown, commit = true) {
75
76
  controller.updateAdjustmentLayer(layer.id, { params: setControlPath(layer.params, path, value) as never }, commit);
@@ -103,17 +104,17 @@
103
104
  }
104
105
  </script>
105
106
 
106
- <div class="flex flex-col gap-2 text-xs" data-testid="adjustment-layers-panel">
107
+ <div class="flex flex-col gap-2 text-xs {className}" data-testid="adjustment-layers-panel">
107
108
  <div class="flex items-center justify-between">
108
- <span class="font-semibold text-slate-200 uppercase tracking-wider text-[11px]">Adjustment Layers</span>
109
+ <span class={imageSectionTitleClass}>Adjustment Layers</span>
109
110
  </div>
110
111
  <div class="flex gap-1.5">
111
- <select aria-label="New adjustment layer kind" class="{selectClass} flex-1" bind:value={newKind}>
112
+ <select aria-label="New adjustment layer kind" class="{imageSelectClass} flex-1" bind:value={newKind}>
112
113
  {#each ADJUSTMENT_KINDS as k (k.kind)}
113
114
  <option value={k.kind}>{k.label}</option>
114
115
  {/each}
115
116
  </select>
116
- <button type="button" class="px-2 py-1 rounded bg-indigo-600 text-white hover:bg-indigo-500" onclick={addLayer}>
117
+ <button type="button" class="px-2 py-1 rounded bg-primary text-primary-foreground hover:bg-primary/90" onclick={addLayer}>
117
118
  Add Layer
118
119
  </button>
119
120
  </div>
@@ -125,19 +126,19 @@
125
126
  {@const label = layer.name || adjustmentKindLabel(layer.kind)}
126
127
  <li
127
128
  class="flex items-center gap-1.5 rounded px-1.5 py-1 {layer.id === selectedId
128
- ? 'bg-indigo-600/20 ring-1 ring-indigo-500/50'
129
- : 'bg-slate-800/60'}"
129
+ ? 'bg-primary/20 ring-1 ring-primary/50'
130
+ : 'bg-muted/60'}"
130
131
  >
131
132
  <input
132
133
  type="checkbox"
133
134
  aria-label="Show {label}"
134
135
  checked={layer.enabled}
135
136
  onchange={() => controller.toggleAdjustmentLayer(layer.id)}
136
- class="accent-indigo-500"
137
+ class="accent-primary"
137
138
  />
138
139
  <button
139
140
  type="button"
140
- class="flex-1 text-left truncate {layer.enabled ? 'text-slate-200' : 'text-slate-500 line-through'}"
141
+ class="flex-1 text-left truncate {layer.enabled ? 'text-foreground' : 'text-muted-foreground line-through'}"
141
142
  aria-pressed={layer.id === selectedId}
142
143
  onclick={() => (selectedId = layer.id === selectedId ? null : layer.id)}
143
144
  >
@@ -148,7 +149,7 @@
148
149
  aria-label="Move {label} up"
149
150
  disabled={index === layers.length - 1}
150
151
  onclick={() => controller.moveAdjustmentLayer(layer.id, index + 1)}
151
- class="px-1 text-slate-400 hover:text-slate-200 disabled:opacity-30"
152
+ class="px-1 text-muted-foreground hover:text-foreground disabled:opacity-30"
152
153
  >
153
154
  ↑
154
155
  </button>
@@ -157,7 +158,7 @@
157
158
  aria-label="Move {label} down"
158
159
  disabled={index === 0}
159
160
  onclick={() => controller.moveAdjustmentLayer(layer.id, index - 1)}
160
- class="px-1 text-slate-400 hover:text-slate-200 disabled:opacity-30"
161
+ class="px-1 text-muted-foreground hover:text-foreground disabled:opacity-30"
161
162
  >
162
163
  ↓
163
164
  </button>
@@ -168,7 +169,7 @@
168
169
  controller.removeAdjustmentLayer(layer.id);
169
170
  if (selectedId === layer.id) selectedId = null;
170
171
  }}
171
- class="px-1 text-rose-400 hover:text-rose-300"
172
+ class="px-1 text-destructive hover:text-destructive"
172
173
  >
173
174
  ✕
174
175
  </button>
@@ -179,12 +180,12 @@
179
180
 
180
181
  {#if selected}
181
182
  {@const layer = selected}
182
- <div class="flex flex-col gap-2 rounded border border-slate-700/60 p-2" data-testid="adjustment-layer-editor">
183
+ <div class="flex flex-col gap-2 rounded border border-border p-2" data-testid="adjustment-layer-editor">
183
184
  <div class="flex items-center gap-2">
184
- <span class="text-slate-400">Blend</span>
185
+ <span class="text-muted-foreground">Blend</span>
185
186
  <select
186
187
  aria-label="Blend mode"
187
- class="{selectClass} flex-1"
188
+ class="{imageSelectClass} flex-1"
188
189
  value={layer.blend}
189
190
  onchange={(e) => controller.updateAdjustmentLayer(layer.id, { blend: (e.target as HTMLSelectElement).value as BlendMode })}
190
191
  >
@@ -244,7 +245,7 @@
244
245
  {/each}
245
246
  </div>
246
247
  {:else if layer.kind === 'photoFilter'}
247
- <label class="flex items-center gap-2 text-slate-300">
248
+ <label class="flex items-center gap-2 text-foreground">
248
249
  Filter colour
249
250
  <input
250
251
  type="color"
@@ -255,7 +256,7 @@
255
256
  />
256
257
  </label>
257
258
  {:else if layer.kind === 'invert'}
258
- <p class="text-slate-500 text-[11px]">Invert has no settings.</p>
259
+ <p class="text-muted-foreground text-[11px]">Invert has no settings.</p>
259
260
  {/if}
260
261
 
261
262
  {#each visibleControls(layer) as spec (spec.id)}
@@ -263,34 +264,34 @@
263
264
  {/each}
264
265
 
265
266
  {#if layer.kind === 'hueSaturation' && range === -1}
266
- <label class="flex items-center gap-2 text-slate-300">
267
+ <label class="flex items-center gap-2 text-foreground">
267
268
  <input
268
269
  type="checkbox"
269
270
  checked={layer.params.colorize}
270
271
  onchange={(e) => setParam(layer, 'colorize', (e.target as HTMLInputElement).checked)}
271
- class="accent-indigo-500"
272
+ class="accent-primary"
272
273
  />
273
274
  Colorize
274
275
  </label>
275
276
  {/if}
276
277
  {#if layer.kind === 'colorBalance' || layer.kind === 'photoFilter'}
277
- <label class="flex items-center gap-2 text-slate-300">
278
+ <label class="flex items-center gap-2 text-foreground">
278
279
  <input
279
280
  type="checkbox"
280
281
  checked={layer.params.preserveLuminosity}
281
282
  onchange={(e) => setParam(layer, 'preserveLuminosity', (e.target as HTMLInputElement).checked)}
282
- class="accent-indigo-500"
283
+ class="accent-primary"
283
284
  />
284
285
  Preserve luminosity
285
286
  </label>
286
287
  {/if}
287
288
  {#if layer.kind === 'blackWhite'}
288
- <label class="flex items-center gap-2 text-slate-300">
289
+ <label class="flex items-center gap-2 text-foreground">
289
290
  <input
290
291
  type="checkbox"
291
292
  checked={layer.params.tint !== null}
292
293
  onchange={(e) => setParam(layer, 'tint', (e.target as HTMLInputElement).checked ? '#e1d3b3' : null)}
293
- class="accent-indigo-500"
294
+ class="accent-primary"
294
295
  />
295
296
  Tint
296
297
  {#if layer.params.tint}
@@ -4,6 +4,8 @@ interface Props {
4
4
  /** The current layers (`state.adjustments.layers`), bottom to top. Omit to follow the controller. */
5
5
  layers?: AdjustmentLayer[];
6
6
  histogram?: ImageHistogram | null;
7
+ /** Extra classes on the root element. */
8
+ class?: string;
7
9
  }
8
10
  declare const AdjustmentLayersPanel: import("svelte").Component<Props, {}, "">;
9
11
  type AdjustmentLayersPanel = ReturnType<typeof AdjustmentLayersPanel>;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { imageChipClass, imageColorInputClass } from './styles.js';
2
3
  import type { ImageEditorController } from '@pixerate/editor/image-editor';
3
4
  import {
4
5
  BRUSH_OPTION_CONTROLS,
@@ -41,7 +42,7 @@
41
42
  });
42
43
  let error = $state<string | null>(null);
43
44
 
44
- const chip = (on: boolean) => `px-2 py-1 rounded text-[11px] ${on ? 'bg-indigo-600 text-white' : 'bg-slate-800 text-slate-300 hover:text-white'}`;
45
+
45
46
 
46
47
  function attempt(fn: () => unknown) {
47
48
  try {
@@ -53,14 +54,14 @@
53
54
  }
54
55
  </script>
55
56
 
56
- <div class="flex flex-col gap-2 text-xs text-slate-200 {className}" data-testid="brush-options">
57
+ <div class="flex flex-col gap-2 text-xs text-foreground {className}" data-testid="brush-options">
57
58
  <div class="flex flex-wrap gap-1" role="toolbar" aria-label="Paint tools">
58
59
  {#each PAINT_TOOLS as t (t.id)}
59
60
  <button
60
61
  type="button"
61
62
  aria-pressed={view.tool === t.id}
62
63
  title={t.shortcut ? `${t.label} (${t.shortcut})` : t.label}
63
- class={chip(view.tool === t.id)}
64
+ class={imageChipClass(view.tool === t.id)}
64
65
  onclick={() => controller.setTool(t.id)}>{t.label}</button>
65
66
  {/each}
66
67
  </div>
@@ -72,9 +73,9 @@
72
73
  aria-label="Foreground colour"
73
74
  value={view.foreground}
74
75
  oninput={(e) => controller.setPaintOptions({ foreground: (e.target as HTMLInputElement).value })}
75
- class="h-6 w-8 rounded border border-slate-700 bg-transparent"
76
+ class={imageColorInputClass}
76
77
  />
77
- <span class="text-slate-400">FG</span>
78
+ <span class="text-muted-foreground">FG</span>
78
79
  </label>
79
80
  <label class="flex items-center gap-1">
80
81
  <input
@@ -82,18 +83,18 @@
82
83
  aria-label="Background colour"
83
84
  value={view.background}
84
85
  oninput={(e) => controller.setPaintOptions({ background: (e.target as HTMLInputElement).value })}
85
- class="h-6 w-8 rounded border border-slate-700 bg-transparent"
86
+ class={imageColorInputClass}
86
87
  />
87
- <span class="text-slate-400">BG</span>
88
+ <span class="text-muted-foreground">BG</span>
88
89
  </label>
89
- <button type="button" title="Swap colours (X)" class={chip(false)} onclick={() => controller.swapPaintColors()}>Swap</button>
90
- <button type="button" title="Default colours (D)" class={chip(false)} onclick={() => controller.resetPaintColors()}>Default</button>
90
+ <button type="button" title="Swap colours (X)" class={imageChipClass(false)} onclick={() => controller.swapPaintColors()}>Swap</button>
91
+ <button type="button" title="Default colours (D)" class={imageChipClass(false)} onclick={() => controller.resetPaintColors()}>Default</button>
91
92
  {#if view.tool === 'brush'}
92
93
  <button
93
94
  type="button"
94
95
  aria-pressed={view.paint.type === 'gradient'}
95
96
  title="Gradient paint: the brush changes colour as the stroke goes"
96
- class={chip(view.paint.type === 'gradient')}
97
+ class={imageChipClass(view.paint.type === 'gradient')}
97
98
  onclick={() => attempt(() => controller.setPaintOptions(view.paint.type === 'gradient' ? { foreground: view.foreground } : { paint: DEFAULT_PAINT_GRADIENT }))}>Gradient</button
98
99
  >
99
100
  {/if}
@@ -108,16 +109,16 @@
108
109
 
109
110
  {#if view.tool === 'mixer'}
110
111
  {@const load = view.mixerLoad}
111
- <div class="flex flex-col gap-2 border-t border-slate-800 pt-2" role="group" aria-label="Mixer Brush" data-testid="mixer-options">
112
+ <div class="flex flex-col gap-2 border-t border-border pt-2" role="group" aria-label="Mixer Brush" data-testid="mixer-options">
112
113
  <div class="flex items-center gap-2">
113
114
  <span
114
115
  aria-label="Brush load"
115
116
  title={load.reservoir ? `Loaded ${rgbUnitToHex(load.reservoir)}, ${Math.round(load.amount * 100)}% left` : 'Unloaded'}
116
- class="h-6 w-6 rounded border border-slate-700"
117
+ class="h-6 w-6 rounded border border-border"
117
118
  style:background={load.reservoir ? rgbUnitToHex(load.reservoir) : 'transparent'}
118
119
  ></span>
119
- <button type="button" title="Fill the reservoir with the foreground colour" class={chip(false)} onclick={() => controller.loadMixerBrush()}>Load brush</button>
120
- <button type="button" title="Empty the picked-up colour" class={chip(false)} onclick={() => controller.cleanMixerBrush()}>Clean brush</button>
120
+ <button type="button" title="Fill the reservoir with the foreground colour" class={imageChipClass(false)} onclick={() => controller.loadMixerBrush()}>Load brush</button>
121
+ <button type="button" title="Empty the picked-up colour" class={imageChipClass(false)} onclick={() => controller.cleanMixerBrush()}>Clean brush</button>
121
122
  </div>
122
123
  <div class="flex gap-1" role="group" aria-label="Mixing">
123
124
  {#each MIXER_MIXING_OPTIONS as m (m.id)}
@@ -125,21 +126,21 @@
125
126
  type="button"
126
127
  aria-pressed={view.mixer.mixing === m.id}
127
128
  title={m.title}
128
- class={chip(view.mixer.mixing === m.id)}
129
+ class={imageChipClass(view.mixer.mixing === m.id)}
129
130
  onclick={() => controller.setPaintOptions({ mixer: { mixing: m.id } })}>{m.label}</button>
130
131
  {/each}
131
132
  </div>
132
133
  {#each MIXER_OPTION_CONTROLS as spec (spec.id)}
133
134
  <ControlSlider {spec} value={view.mixer[spec.id]} onchange={(v) => controller.setPaintOptions({ mixer: { [spec.id]: v } })} />
134
135
  {/each}
135
- <div class="flex flex-wrap gap-3 text-slate-300">
136
+ <div class="flex flex-wrap gap-3 text-foreground">
136
137
  <label class="flex items-center gap-1.5">
137
138
  <input
138
139
  type="checkbox"
139
140
  aria-label="Load the brush after each stroke"
140
141
  checked={view.mixer.loadEachStroke}
141
142
  onchange={(e) => controller.setPaintOptions({ mixer: { loadEachStroke: (e.target as HTMLInputElement).checked } })}
142
- class="accent-indigo-500"
143
+ class="accent-primary"
143
144
  />
144
145
  Load each stroke
145
146
  </label>
@@ -149,7 +150,7 @@
149
150
  aria-label="Clean the brush after each stroke"
150
151
  checked={view.mixer.cleanEachStroke}
151
152
  onchange={(e) => controller.setPaintOptions({ mixer: { cleanEachStroke: (e.target as HTMLInputElement).checked } })}
152
- class="accent-indigo-500"
153
+ class="accent-primary"
153
154
  />
154
155
  Clean each stroke
155
156
  </label>
@@ -158,12 +159,12 @@
158
159
  {/if}
159
160
 
160
161
  {#if isRetouchPaintTool(view.tool)}
161
- <div class="flex flex-col gap-2 border-t border-slate-800 pt-2" role="group" aria-label="Retouch options" data-testid="retouch-options">
162
+ <div class="flex flex-col gap-2 border-t border-border pt-2" role="group" aria-label="Retouch options" data-testid="retouch-options">
162
163
  {#if view.tool === 'clone' || view.tool === 'heal'}
163
164
  <div class="flex items-center gap-2">
164
- <span class="text-slate-400" data-testid="clone-source">{view.sourceLabel}</span>
165
+ <span class="text-muted-foreground" data-testid="clone-source">{view.sourceLabel}</span>
165
166
  {#if view.cloneSource}
166
- <button type="button" title="Forget the source point" class={chip(false)} onclick={() => controller.setCloneSource(null)}>Clear source</button>
167
+ <button type="button" title="Forget the source point" class={imageChipClass(false)} onclick={() => controller.setCloneSource(null)}>Clear source</button>
167
168
  {/if}
168
169
  </div>
169
170
  <div class="flex flex-col gap-1" role="group" aria-label="Clone source transform" data-testid="clone-transform">
@@ -171,23 +172,23 @@
171
172
  <ControlSlider {spec} value={view.cloneView[spec.id]} onchange={(v) => setCloneTransformValue(controller, spec.id, v)} />
172
173
  {/each}
173
174
  <div class="flex items-center gap-1">
174
- <button type="button" aria-pressed={view.cloneView.flipH} title="Mirror the source left to right" class={chip(view.cloneView.flipH)} onclick={() => setCloneTransformValue(controller, 'flipH')}>Flip H</button>
175
- <button type="button" aria-pressed={view.cloneView.flipV} title="Mirror the source top to bottom" class={chip(view.cloneView.flipV)} onclick={() => setCloneTransformValue(controller, 'flipV')}>Flip V</button>
175
+ <button type="button" aria-pressed={view.cloneView.flipH} title="Mirror the source left to right" class={imageChipClass(view.cloneView.flipH)} onclick={() => setCloneTransformValue(controller, 'flipH')}>Flip H</button>
176
+ <button type="button" aria-pressed={view.cloneView.flipV} title="Mirror the source top to bottom" class={imageChipClass(view.cloneView.flipV)} onclick={() => setCloneTransformValue(controller, 'flipV')}>Flip V</button>
176
177
  {#if !view.cloneView.identity}
177
- <button type="button" title="Back to 100 %, no flip, 0°" class={chip(false)} onclick={() => controller.setCloneTransform(null)}>Reset transform</button>
178
+ <button type="button" title="Back to 100 %, no flip, 0°" class={imageChipClass(false)} onclick={() => controller.setCloneTransform(null)}>Reset transform</button>
178
179
  {/if}
179
180
  </div>
180
181
  </div>
181
182
  {/if}
182
183
  {#each RETOUCH_CHOICES.filter((c) => c.tools.includes(view.tool as never)) as choice (choice.id)}
183
184
  <div class="flex flex-wrap items-center gap-1" role="group" aria-label={choice.label}>
184
- <span class="text-slate-400">{choice.label}</span>
185
+ <span class="text-muted-foreground">{choice.label}</span>
185
186
  {#each choice.options as o (o.id)}
186
187
  <button
187
188
  type="button"
188
189
  aria-pressed={view.retouch[choice.id] === o.id}
189
190
  title={o.title}
190
- class={chip(view.retouch[choice.id] === o.id)}
191
+ class={imageChipClass(view.retouch[choice.id] === o.id)}
191
192
  onclick={() => setRetouchChoice(controller, choice.id, o.id)}>{o.label}</button>
192
193
  {/each}
193
194
  </div>
@@ -195,7 +196,7 @@
195
196
  {#each RETOUCH_OPTION_CONTROLS.filter((c) => c.tools.includes(view.tool as never)) as spec (spec.id)}
196
197
  <ControlSlider {spec} value={view.retouch[spec.id]} onchange={(v) => controller.setPaintOptions({ retouch: { [spec.id]: v } })} />
197
198
  {/each}
198
- <div class="flex flex-wrap gap-3 text-slate-300">
199
+ <div class="flex flex-wrap gap-3 text-foreground">
199
200
  {#each RETOUCH_TOGGLES.filter((t) => t.tools.includes(view.tool as never)) as t (t.id)}
200
201
  <label class="flex items-center gap-1.5">
201
202
  <input
@@ -203,7 +204,7 @@
203
204
  aria-label={t.label}
204
205
  checked={view.retouch[t.id]}
205
206
  onchange={(e) => controller.setPaintOptions({ retouch: { [t.id]: (e.target as HTMLInputElement).checked } })}
206
- class="accent-indigo-500"
207
+ class="accent-primary"
207
208
  />
208
209
  {t.label}
209
210
  </label>
@@ -212,14 +213,14 @@
212
213
  </div>
213
214
  {/if}
214
215
 
215
- <div class="flex flex-wrap gap-3 text-slate-300" role="group" aria-label="Pressure">
216
+ <div class="flex flex-wrap gap-3 text-foreground" role="group" aria-label="Pressure">
216
217
  <label class="flex items-center gap-1.5">
217
218
  <input
218
219
  type="checkbox"
219
220
  aria-label="Pressure controls size"
220
221
  checked={view.brush.pressureSize}
221
222
  onchange={(e) => controller.setPaintOptions({ brush: { pressureSize: (e.target as HTMLInputElement).checked } })}
222
- class="accent-indigo-500"
223
+ class="accent-primary"
223
224
  />
224
225
  Pressure size
225
226
  </label>
@@ -229,17 +230,17 @@
229
230
  aria-label="Pressure controls opacity"
230
231
  checked={view.brush.pressureOpacity}
231
232
  onchange={(e) => controller.setPaintOptions({ brush: { pressureOpacity: (e.target as HTMLInputElement).checked } })}
232
- class="accent-indigo-500"
233
+ class="accent-primary"
233
234
  />
234
235
  Pressure opacity
235
236
  </label>
236
237
  </div>
237
238
 
238
239
  <div class="flex gap-1">
239
- <button type="button" class={chip(false)} onclick={() => attempt(() => controller.fillPixels())}>Fill</button>
240
- <button type="button" class={chip(false)} onclick={() => attempt(() => controller.clearPixels())}>Clear</button>
240
+ <button type="button" class={imageChipClass(false)} onclick={() => attempt(() => controller.fillPixels())}>Fill</button>
241
+ <button type="button" class={imageChipClass(false)} onclick={() => attempt(() => controller.clearPixels())}>Clear</button>
241
242
  </div>
242
243
  {#if error}
243
- <p role="alert" class="text-rose-400">{error}</p>
244
+ <p role="alert" class="text-destructive">{error}</p>
244
245
  {/if}
245
246
  </div>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { imageChipClass } from './styles.js';
2
3
  import type { ImageEditorController, Intent } from '@pixerate/editor/image-editor';
3
4
  import { COLOR_MODE_OPTIONS, INTENT_OPTIONS, PROOF_PROFILE_OPTIONS, WORKING_SPACE_OPTIONS, colorPanelView } from '@pixerate/editor/image-editor';
4
5
  import { untrack } from 'svelte';
@@ -24,9 +25,8 @@
24
25
  let intent = $state<Intent>('relative');
25
26
  let bpc = $state(true);
26
27
  let error = $state<string | null>(null);
27
-
28
- const chip = 'px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-300 hover:text-white';
29
- const select = 'rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-200';
28
+ const chip = imageChipClass(false);
29
+ const select = 'rounded bg-muted px-1 py-0.5 text-[11px] text-foreground';
30
30
 
31
31
  function attempt(fn: () => unknown) {
32
32
  try {
@@ -51,20 +51,20 @@
51
51
  }
52
52
  </script>
53
53
 
54
- <div class="flex flex-col gap-2 text-xs text-slate-200 {className}" data-testid="color-profile-panel">
54
+ <div class="flex flex-col gap-2 text-xs text-foreground {className}" data-testid="color-profile-panel">
55
55
  <div class="flex items-center gap-2">
56
- <span class="text-slate-400">Profile</span>
56
+ <span class="text-muted-foreground">Profile</span>
57
57
  <span data-testid="color-profile-name">{view.profileName}</span>
58
58
  </div>
59
59
  <div class="flex flex-wrap items-center gap-1" role="group" aria-label="Colour mode">
60
- <span class="text-slate-400">Mode</span>
60
+ <span class="text-muted-foreground">Mode</span>
61
61
  <select aria-label="Colour mode" class={select} value={view.mode} onchange={(e) => attempt(() => controller.setColorMode((e.target as HTMLSelectElement).value as 'rgb' | 'cmyk' | 'lab'))}>
62
62
  {#each COLOR_MODE_OPTIONS as o (o.id)}
63
63
  <option value={o.id} title={o.title}>{o.label}</option>
64
64
  {/each}
65
65
  </select>
66
66
  {#if view.cmykProfileName}
67
- <span class="text-slate-400">{view.cmykProfileName}</span>
67
+ <span class="text-muted-foreground">{view.cmykProfileName}</span>
68
68
  {/if}
69
69
  </div>
70
70
  <div class="flex flex-wrap items-center gap-1" role="group" aria-label="Assign or convert">
@@ -79,14 +79,14 @@
79
79
  {/each}
80
80
  </select>
81
81
  <label class="flex items-center gap-1">
82
- <input type="checkbox" aria-label="Black point compensation" checked={bpc} onchange={(e) => (bpc = (e.target as HTMLInputElement).checked)} class="accent-indigo-500" />
82
+ <input type="checkbox" aria-label="Black point compensation" checked={bpc} onchange={(e) => (bpc = (e.target as HTMLInputElement).checked)} class="accent-primary" />
83
83
  BPC
84
84
  </label>
85
85
  <button type="button" class={chip} title="Edit › Assign Profile: keep the numbers" onclick={() => attempt(() => controller.assignProfile(target))}>Assign</button>
86
86
  <button type="button" class={chip} title="Edit › Convert to Profile: keep the look" onclick={() => attempt(() => controller.convertToProfile(target, { intent, bpc }))}>Convert</button>
87
87
  </div>
88
88
  <div class="flex flex-wrap items-center gap-1" role="group" aria-label="Proof setup">
89
- <span class="text-slate-400">Proof</span>
89
+ <span class="text-muted-foreground">Proof</span>
90
90
  <select
91
91
  aria-label="Proof profile"
92
92
  class={select}
@@ -108,16 +108,16 @@
108
108
  {/each}
109
109
  </select>
110
110
  <label class="flex items-center gap-1">
111
- <input type="checkbox" aria-label="Simulate paper colour" checked={view.proof.simulatePaper} onchange={(e) => proofWith({ simulatePaper: (e.target as HTMLInputElement).checked })} class="accent-indigo-500" />
111
+ <input type="checkbox" aria-label="Simulate paper colour" checked={view.proof.simulatePaper} onchange={(e) => proofWith({ simulatePaper: (e.target as HTMLInputElement).checked })} class="accent-primary" />
112
112
  Paper
113
113
  </label>
114
114
  <label class="flex items-center gap-1">
115
- <input type="checkbox" aria-label="Gamut warning" checked={view.proof.gamutWarning} onchange={(e) => proofWith({ gamutWarning: (e.target as HTMLInputElement).checked })} class="accent-indigo-500" />
115
+ <input type="checkbox" aria-label="Gamut warning" checked={view.proof.gamutWarning} onchange={(e) => proofWith({ gamutWarning: (e.target as HTMLInputElement).checked })} class="accent-primary" />
116
116
  Gamut warning
117
117
  </label>
118
118
  {/if}
119
119
  </div>
120
120
  {#if error}
121
- <p role="alert" class="text-rose-400">{error}</p>
121
+ <p role="alert" class="text-destructive">{error}</p>
122
122
  {/if}
123
123
  </div>
@@ -9,16 +9,18 @@
9
9
  disabled?: boolean;
10
10
  /** Overrides `spec.label` (also the accessible name). */
11
11
  label?: string;
12
+ /** Extra classes on the root element. */
13
+ class?: string;
12
14
  }
13
15
 
14
- let { spec, value, onchange, disabled = false, label }: Props = $props();
16
+ let { spec, value, onchange, disabled = false, label, class: className = '' }: Props = $props();
15
17
 
16
18
  const name = $derived(label ?? spec.label);
17
19
  const track = $derived(controlTrackBackground(spec));
18
20
  </script>
19
21
 
20
- <div data-control-id={spec.id}>
21
- <div class="flex justify-between text-slate-400 mb-1">
22
+ <div data-control-id={spec.id} class={className || undefined}>
23
+ <div class="flex justify-between text-muted-foreground mb-1">
22
24
  <span>{name}</span>
23
25
  <span class="font-mono">{formatControlValue(spec, value)}</span>
24
26
  </div>
@@ -31,7 +33,7 @@
31
33
  {disabled}
32
34
  {value}
33
35
  oninput={(e) => onchange(Number((e.target as HTMLInputElement).value))}
34
- class="w-full accent-indigo-500 disabled:opacity-40"
36
+ class="w-full accent-primary disabled:opacity-40"
35
37
  />
36
38
  {#if track}
37
39
  <div class="h-0.5 rounded-full opacity-70" style="background: {track}"></div>
@@ -6,6 +6,8 @@ interface Props {
6
6
  disabled?: boolean;
7
7
  /** Overrides `spec.label` (also the accessible name). */
8
8
  label?: string;
9
+ /** Extra classes on the root element. */
10
+ class?: string;
9
11
  }
10
12
  declare const ControlSlider: import("svelte").Component<Props, {}, "">;
11
13
  type ControlSlider = ReturnType<typeof ControlSlider>;
@@ -18,12 +18,14 @@
18
18
  histogram?: ImageHistogram | null;
19
19
  channel?: 'master' | 'red' | 'green' | 'blue';
20
20
  size?: number;
21
+ /** Extra classes on the root element. */
22
+ class?: string;
21
23
  }
22
24
 
23
- let { points, onchange, histogram = null, channel = 'master', size = 200 }: Props = $props();
25
+ let { points, onchange, histogram = null, channel = 'master', size = 200, class: className = '' }: Props = $props();
24
26
 
25
27
  const BOX = 256;
26
- const STROKE = { master: '#e2e8f0', red: '#ef4444', green: '#22c55e', blue: '#3b82f6' };
28
+ const STROKE = { master: 'var(--foreground, #e2e8f0)', red: '#ef4444', green: '#22c55e', blue: '#3b82f6' };
27
29
 
28
30
  let svgEl: SVGSVGElement | undefined = $state();
29
31
  let drag: number | null = $state(null);
@@ -72,7 +74,7 @@
72
74
  role="application"
73
75
  aria-label="{channel} curve"
74
76
  data-testid="curves-editor"
75
- class="rounded bg-slate-950/70 touch-none select-none cursor-crosshair"
77
+ class="rounded bg-muted/40 touch-none select-none cursor-crosshair {className}"
76
78
  {onpointerdown}
77
79
  {onpointermove}
78
80
  onpointerup={() => (drag = null)}
@@ -96,7 +98,7 @@
96
98
  cx={b.x}
97
99
  cy={b.y}
98
100
  r="5"
99
- fill={drag === i ? STROKE[channel] : '#0f172a'}
101
+ fill={drag === i ? STROKE[channel] : 'var(--background, #0f172a)'}
100
102
  stroke={STROKE[channel]}
101
103
  stroke-width="2"
102
104
  aria-label="Point {p.input} to {p.output}"
@@ -6,6 +6,8 @@ interface Props {
6
6
  histogram?: ImageHistogram | null;
7
7
  channel?: 'master' | 'red' | 'green' | 'blue';
8
8
  size?: number;
9
+ /** Extra classes on the root element. */
10
+ class?: string;
9
11
  }
10
12
  declare const CurvesEditor: import("svelte").Component<Props, {}, "">;
11
13
  type CurvesEditor = ReturnType<typeof CurvesEditor>;