@pixerate/editor-svelte 0.22.0 → 0.24.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 (163) hide show
  1. package/README.md +6 -2
  2. package/dist/MergePreview.svelte +201 -0
  3. package/dist/MergePreview.svelte.d.ts +25 -0
  4. package/dist/MergePreview.svelte.d.ts.map +1 -0
  5. package/dist/StylesGallery.svelte +169 -0
  6. package/dist/StylesGallery.svelte.d.ts +27 -0
  7. package/dist/StylesGallery.svelte.d.ts.map +1 -0
  8. package/dist/TemplateRenderer.svelte +4 -10
  9. package/dist/TemplateRenderer.svelte.d.ts.map +1 -1
  10. package/dist/canvas/TokenBindingPicker.svelte +93 -0
  11. package/dist/canvas/TokenBindingPicker.svelte.d.ts +20 -0
  12. package/dist/canvas/TokenBindingPicker.svelte.d.ts.map +1 -0
  13. package/dist/canvas/TokenModeSwitcher.svelte +55 -0
  14. package/dist/canvas/TokenModeSwitcher.svelte.d.ts +18 -0
  15. package/dist/canvas/TokenModeSwitcher.svelte.d.ts.map +1 -0
  16. package/dist/canvas/createCanvasArrange.svelte.d.ts +4 -0
  17. package/dist/canvas/createCanvasArrange.svelte.d.ts.map +1 -1
  18. package/dist/canvas/createCanvasAutoLayout.svelte.d.ts +68 -0
  19. package/dist/canvas/createCanvasAutoLayout.svelte.d.ts.map +1 -0
  20. package/dist/canvas/createCanvasAutoLayout.svelte.js +78 -0
  21. package/dist/canvas/createCanvasTokens.svelte.d.ts +31 -0
  22. package/dist/canvas/createCanvasTokens.svelte.d.ts.map +1 -0
  23. package/dist/canvas/createCanvasTokens.svelte.js +50 -0
  24. package/dist/canvas/index.d.ts +7 -0
  25. package/dist/canvas/index.d.ts.map +1 -1
  26. package/dist/canvas/index.js +10 -0
  27. package/dist/fileImport.svelte.d.ts +8 -1
  28. package/dist/fileImport.svelte.d.ts.map +1 -1
  29. package/dist/fileImport.svelte.js +11 -1
  30. package/dist/find-replace/FindReplaceBar.svelte +203 -0
  31. package/dist/find-replace/FindReplaceBar.svelte.d.ts +15 -0
  32. package/dist/find-replace/FindReplaceBar.svelte.d.ts.map +1 -0
  33. package/dist/find-replace/findReplace.svelte.d.ts +27 -0
  34. package/dist/find-replace/findReplace.svelte.d.ts.map +1 -0
  35. package/dist/find-replace/findReplace.svelte.js +79 -0
  36. package/dist/find-replace/index.d.ts +6 -0
  37. package/dist/find-replace/index.d.ts.map +1 -0
  38. package/dist/find-replace/index.js +2 -0
  39. package/dist/generation/AssetMentionChips.svelte +130 -0
  40. package/dist/generation/AssetMentionChips.svelte.d.ts +21 -0
  41. package/dist/generation/AssetMentionChips.svelte.d.ts.map +1 -0
  42. package/dist/generation/GenerationHistory.svelte +161 -0
  43. package/dist/generation/GenerationHistory.svelte.d.ts +28 -0
  44. package/dist/generation/GenerationHistory.svelte.d.ts.map +1 -0
  45. package/dist/generation/GenerationSettings.svelte +174 -0
  46. package/dist/generation/GenerationSettings.svelte.d.ts +23 -0
  47. package/dist/generation/GenerationSettings.svelte.d.ts.map +1 -0
  48. package/dist/generation/JobProgress.svelte +53 -0
  49. package/dist/generation/JobProgress.svelte.d.ts +17 -0
  50. package/dist/generation/JobProgress.svelte.d.ts.map +1 -0
  51. package/dist/generation/index.d.ts +17 -0
  52. package/dist/generation/index.d.ts.map +1 -0
  53. package/dist/generation/index.js +7 -0
  54. package/dist/generation/plan.svelte.d.ts +16 -0
  55. package/dist/generation/plan.svelte.d.ts.map +1 -0
  56. package/dist/generation/plan.svelte.js +26 -0
  57. package/dist/generation/progress.svelte.d.ts +25 -0
  58. package/dist/generation/progress.svelte.d.ts.map +1 -0
  59. package/dist/generation/progress.svelte.js +36 -0
  60. package/dist/generation/records.svelte.d.ts +18 -0
  61. package/dist/generation/records.svelte.d.ts.map +1 -0
  62. package/dist/generation/records.svelte.js +66 -0
  63. package/dist/image-editor/AdjustmentLayersPanel.svelte +309 -0
  64. package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts +11 -0
  65. package/dist/image-editor/AdjustmentLayersPanel.svelte.d.ts.map +1 -0
  66. package/dist/image-editor/ControlSlider.svelte +39 -0
  67. package/dist/image-editor/ControlSlider.svelte.d.ts +13 -0
  68. package/dist/image-editor/ControlSlider.svelte.d.ts.map +1 -0
  69. package/dist/image-editor/CurvesEditor.svelte +105 -0
  70. package/dist/image-editor/CurvesEditor.svelte.d.ts +13 -0
  71. package/dist/image-editor/CurvesEditor.svelte.d.ts.map +1 -0
  72. package/dist/image-editor/Histogram.svelte +49 -0
  73. package/dist/image-editor/Histogram.svelte.d.ts +12 -0
  74. package/dist/image-editor/Histogram.svelte.d.ts.map +1 -0
  75. package/dist/image-editor/ImageEditor.svelte +104 -141
  76. package/dist/image-editor/ImageEditor.svelte.d.ts.map +1 -1
  77. package/dist/image-editor/RefineEdgePanel.svelte +180 -0
  78. package/dist/image-editor/RefineEdgePanel.svelte.d.ts +13 -0
  79. package/dist/image-editor/RefineEdgePanel.svelte.d.ts.map +1 -0
  80. package/dist/image-editor/SelectionOptionsBar.svelte +135 -0
  81. package/dist/image-editor/SelectionOptionsBar.svelte.d.ts +15 -0
  82. package/dist/image-editor/SelectionOptionsBar.svelte.d.ts.map +1 -0
  83. package/dist/image-editor/imageEditorState.svelte.d.ts +14 -1
  84. package/dist/image-editor/imageEditorState.svelte.d.ts.map +1 -1
  85. package/dist/image-editor/imageEditorState.svelte.js +40 -0
  86. package/dist/image-editor/index.d.ts +18 -0
  87. package/dist/image-editor/index.d.ts.map +1 -1
  88. package/dist/image-editor/index.js +6 -0
  89. package/dist/index.d.ts +12 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +7 -0
  92. package/dist/merge.svelte.d.ts +51 -0
  93. package/dist/merge.svelte.d.ts.map +1 -0
  94. package/dist/merge.svelte.js +117 -0
  95. package/dist/paragraphStyles.svelte.d.ts +30 -0
  96. package/dist/paragraphStyles.svelte.d.ts.map +1 -0
  97. package/dist/paragraphStyles.svelte.js +44 -0
  98. package/dist/persistence/RecoveryPrompt.svelte +57 -0
  99. package/dist/persistence/RecoveryPrompt.svelte.d.ts +18 -0
  100. package/dist/persistence/RecoveryPrompt.svelte.d.ts.map +1 -0
  101. package/dist/persistence/autosave.svelte.d.ts +63 -0
  102. package/dist/persistence/autosave.svelte.d.ts.map +1 -0
  103. package/dist/persistence/autosave.svelte.js +110 -0
  104. package/dist/persistence/index.d.ts +6 -0
  105. package/dist/persistence/index.d.ts.map +1 -0
  106. package/dist/persistence/index.js +2 -0
  107. package/dist/review/TrackedChangesList.svelte +67 -0
  108. package/dist/review/TrackedChangesList.svelte.d.ts +13 -0
  109. package/dist/review/TrackedChangesList.svelte.d.ts.map +1 -0
  110. package/dist/review/documentCompare.svelte.d.ts +21 -0
  111. package/dist/review/documentCompare.svelte.d.ts.map +1 -0
  112. package/dist/review/documentCompare.svelte.js +23 -0
  113. package/dist/review/index.d.ts +4 -0
  114. package/dist/review/index.d.ts.map +1 -1
  115. package/dist/review/index.js +2 -0
  116. package/dist/review/review.svelte.d.ts +6 -0
  117. package/dist/review/review.svelte.d.ts.map +1 -1
  118. package/dist/review/review.svelte.js +3 -0
  119. package/dist/spreadsheet/FormulaBar.svelte +13 -4
  120. package/dist/spreadsheet/FormulaBar.svelte.d.ts.map +1 -1
  121. package/dist/spreadsheet/FormulaHints.svelte +7 -5
  122. package/dist/spreadsheet/FormulaHints.svelte.d.ts +3 -0
  123. package/dist/spreadsheet/FormulaHints.svelte.d.ts.map +1 -1
  124. package/dist/spreadsheet/NameManager.svelte +165 -0
  125. package/dist/spreadsheet/NameManager.svelte.d.ts +11 -0
  126. package/dist/spreadsheet/NameManager.svelte.d.ts.map +1 -0
  127. package/dist/spreadsheet/SheetTabs.svelte +161 -0
  128. package/dist/spreadsheet/SheetTabs.svelte.d.ts +10 -0
  129. package/dist/spreadsheet/SheetTabs.svelte.d.ts.map +1 -0
  130. package/dist/spreadsheet/SpreadsheetConditionalFormatPanel.svelte +184 -0
  131. package/dist/spreadsheet/SpreadsheetConditionalFormatPanel.svelte.d.ts +9 -0
  132. package/dist/spreadsheet/SpreadsheetConditionalFormatPanel.svelte.d.ts.map +1 -0
  133. package/dist/spreadsheet/SpreadsheetContextMenu.svelte +13 -5
  134. package/dist/spreadsheet/SpreadsheetContextMenu.svelte.d.ts +5 -1
  135. package/dist/spreadsheet/SpreadsheetContextMenu.svelte.d.ts.map +1 -1
  136. package/dist/spreadsheet/SpreadsheetDataValidationPanel.svelte +141 -0
  137. package/dist/spreadsheet/SpreadsheetDataValidationPanel.svelte.d.ts +9 -0
  138. package/dist/spreadsheet/SpreadsheetDataValidationPanel.svelte.d.ts.map +1 -0
  139. package/dist/spreadsheet/SpreadsheetEditor.svelte +199 -11
  140. package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts +2 -0
  141. package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts.map +1 -1
  142. package/dist/spreadsheet/SpreadsheetValidationDialog.svelte +92 -0
  143. package/dist/spreadsheet/SpreadsheetValidationDialog.svelte.d.ts +9 -0
  144. package/dist/spreadsheet/SpreadsheetValidationDialog.svelte.d.ts.map +1 -0
  145. package/dist/spreadsheet/index.d.ts +9 -0
  146. package/dist/spreadsheet/index.d.ts.map +1 -1
  147. package/dist/spreadsheet/index.js +5 -0
  148. package/dist/spreadsheet/spreadsheet.svelte.d.ts +57 -2
  149. package/dist/spreadsheet/spreadsheet.svelte.d.ts.map +1 -1
  150. package/dist/spreadsheet/spreadsheet.svelte.js +105 -6
  151. package/dist/tokens/TokenSwatch.svelte +14 -0
  152. package/dist/tokens/TokenSwatch.svelte.d.ts +8 -0
  153. package/dist/tokens/TokenSwatch.svelte.d.ts.map +1 -0
  154. package/dist/tokens/TokensPanel.svelte +323 -0
  155. package/dist/tokens/TokensPanel.svelte.d.ts +14 -0
  156. package/dist/tokens/TokensPanel.svelte.d.ts.map +1 -0
  157. package/dist/tokens/createTokens.svelte.d.ts +31 -0
  158. package/dist/tokens/createTokens.svelte.d.ts.map +1 -0
  159. package/dist/tokens/createTokens.svelte.js +43 -0
  160. package/dist/tokens/index.d.ts +7 -0
  161. package/dist/tokens/index.d.ts.map +1 -0
  162. package/dist/tokens/index.js +3 -0
  163. package/package.json +26 -2
@@ -0,0 +1,309 @@
1
+ <script lang="ts">
2
+ import type {
3
+ AdjustmentKind,
4
+ AdjustmentLayer,
5
+ BlendMode,
6
+ ControlSpec,
7
+ CurvePoint,
8
+ ImageEditorController,
9
+ ImageHistogram,
10
+ } from '@pixerate/editor/image-editor';
11
+ import {
12
+ ADJUSTMENT_KINDS,
13
+ ADJUSTMENT_LAYER_CONTROLS,
14
+ ADJUSTMENT_LAYER_OPACITY_CONTROL,
15
+ BLEND_MODES,
16
+ HUE_RANGE_LABELS,
17
+ adjustmentKindLabel,
18
+ getControlPath,
19
+ hueRangeWeight,
20
+ setControlPath,
21
+ } from '@pixerate/editor/image-editor';
22
+ import { untrack } from 'svelte';
23
+ import ControlSlider from './ControlSlider.svelte';
24
+ import CurvesEditor from './CurvesEditor.svelte';
25
+
26
+ interface Props {
27
+ controller: ImageEditorController;
28
+ /** The current layers (`state.adjustments.layers`), bottom to top. Omit to follow the controller. */
29
+ layers?: AdjustmentLayer[];
30
+ histogram?: ImageHistogram | null;
31
+ }
32
+
33
+ let { controller, layers: layersProp, histogram = null }: Props = $props();
34
+
35
+ // Follows the controller when no `layers` prop is given.
36
+ let tick = $state(0);
37
+ $effect(() => {
38
+ const c = controller;
39
+ // `subscribe` calls the listener at once: untracked, or the effect would depend on `tick`.
40
+ return untrack(() => c.subscribe(() => tick++));
41
+ });
42
+ const layers = $derived.by(() => {
43
+ void tick;
44
+ return layersProp ?? controller.getAdjustmentLayers();
45
+ });
46
+
47
+ type Channel = 'master' | 'red' | 'green' | 'blue';
48
+ const CHANNELS: { id: Channel; label: string }[] = [
49
+ { id: 'master', label: 'RGB' },
50
+ { id: 'red', label: 'Red' },
51
+ { id: 'green', label: 'Green' },
52
+ { id: 'blue', label: 'Blue' },
53
+ ];
54
+ const TONES = [
55
+ { id: 'shadows', label: 'Shadows' },
56
+ { id: 'midtones', label: 'Midtones' },
57
+ { id: 'highlights', label: 'Highlights' },
58
+ ] as const;
59
+ type Tone = (typeof TONES)[number]['id'];
60
+
61
+ let newKind: AdjustmentKind = $state('curves');
62
+ let selectedId: string | null = $state(null);
63
+ let channel: Channel = $state('master');
64
+ let range = $state(-1);
65
+ let tone: Tone = $state('midtones');
66
+
67
+ const selected = $derived(layers.find((l) => l.id === selectedId) ?? null);
68
+ 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
+ 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
+
74
+ function setParam(layer: AdjustmentLayer, path: string, value: unknown, commit = true) {
75
+ controller.updateAdjustmentLayer(layer.id, { params: setControlPath(layer.params, path, value) as never }, commit);
76
+ }
77
+ const paramValue = (layer: AdjustmentLayer, spec: ControlSpec) => Number(getControlPath(layer.params, spec.id) ?? spec.default);
78
+
79
+ function visibleControls(layer: AdjustmentLayer): ControlSpec[] {
80
+ const all = ADJUSTMENT_LAYER_CONTROLS[layer.kind];
81
+ switch (layer.kind) {
82
+ case 'levels':
83
+ return all.filter((s) => s.id.startsWith(`${channel}.`));
84
+ case 'hueSaturation':
85
+ return all.filter((s) =>
86
+ range === -1 || layer.params.colorize ? !s.id.startsWith('ranges.') : s.id.startsWith(`ranges.${range}.`)
87
+ );
88
+ case 'colorBalance':
89
+ return all.filter((s) => s.id.startsWith(`${tone}.`));
90
+ default:
91
+ return [...all];
92
+ }
93
+ }
94
+
95
+ function stripPoints(bounds: [number, number, number, number]) {
96
+ return Array.from({ length: 73 }, (_, i) => `${(i / 72) * 360},${20 - hueRangeWeight({ bounds }, i * 5) * 18}`).join(' ');
97
+ }
98
+
99
+ function addLayer() {
100
+ selectedId = controller.addAdjustmentLayer(newKind);
101
+ channel = 'master';
102
+ range = -1;
103
+ }
104
+ </script>
105
+
106
+ <div class="flex flex-col gap-2 text-xs" data-testid="adjustment-layers-panel">
107
+ <div class="flex items-center justify-between">
108
+ <span class="font-semibold text-slate-200 uppercase tracking-wider text-[11px]">Adjustment Layers</span>
109
+ </div>
110
+ <div class="flex gap-1.5">
111
+ <select aria-label="New adjustment layer kind" class="{selectClass} flex-1" bind:value={newKind}>
112
+ {#each ADJUSTMENT_KINDS as k (k.kind)}
113
+ <option value={k.kind}>{k.label}</option>
114
+ {/each}
115
+ </select>
116
+ <button type="button" class="px-2 py-1 rounded bg-indigo-600 text-white hover:bg-indigo-500" onclick={addLayer}>
117
+ Add Layer
118
+ </button>
119
+ </div>
120
+
121
+ {#if topFirst.length > 0}
122
+ <ul class="flex flex-col gap-1" aria-label="Adjustment layers">
123
+ {#each topFirst as layer (layer.id)}
124
+ {@const index = layers.indexOf(layer)}
125
+ {@const label = layer.name || adjustmentKindLabel(layer.kind)}
126
+ <li
127
+ 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'}"
130
+ >
131
+ <input
132
+ type="checkbox"
133
+ aria-label="Show {label}"
134
+ checked={layer.enabled}
135
+ onchange={() => controller.toggleAdjustmentLayer(layer.id)}
136
+ class="accent-indigo-500"
137
+ />
138
+ <button
139
+ type="button"
140
+ class="flex-1 text-left truncate {layer.enabled ? 'text-slate-200' : 'text-slate-500 line-through'}"
141
+ aria-pressed={layer.id === selectedId}
142
+ onclick={() => (selectedId = layer.id === selectedId ? null : layer.id)}
143
+ >
144
+ {label}
145
+ </button>
146
+ <button
147
+ type="button"
148
+ aria-label="Move {label} up"
149
+ disabled={index === layers.length - 1}
150
+ onclick={() => controller.moveAdjustmentLayer(layer.id, index + 1)}
151
+ class="px-1 text-slate-400 hover:text-slate-200 disabled:opacity-30"
152
+ >
153
+ ↑
154
+ </button>
155
+ <button
156
+ type="button"
157
+ aria-label="Move {label} down"
158
+ disabled={index === 0}
159
+ onclick={() => controller.moveAdjustmentLayer(layer.id, index - 1)}
160
+ class="px-1 text-slate-400 hover:text-slate-200 disabled:opacity-30"
161
+ >
162
+ ↓
163
+ </button>
164
+ <button
165
+ type="button"
166
+ aria-label="Remove {label}"
167
+ onclick={() => {
168
+ controller.removeAdjustmentLayer(layer.id);
169
+ if (selectedId === layer.id) selectedId = null;
170
+ }}
171
+ class="px-1 text-rose-400 hover:text-rose-300"
172
+ >
173
+ ✕
174
+ </button>
175
+ </li>
176
+ {/each}
177
+ </ul>
178
+ {/if}
179
+
180
+ {#if selected}
181
+ {@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 items-center gap-2">
184
+ <span class="text-slate-400">Blend</span>
185
+ <select
186
+ aria-label="Blend mode"
187
+ class="{selectClass} flex-1"
188
+ value={layer.blend}
189
+ onchange={(e) => controller.updateAdjustmentLayer(layer.id, { blend: (e.target as HTMLSelectElement).value as BlendMode })}
190
+ >
191
+ {#each BLEND_MODES as m (m.id)}
192
+ <option value={m.id}>{m.label}</option>
193
+ {/each}
194
+ </select>
195
+ </div>
196
+ <ControlSlider
197
+ spec={ADJUSTMENT_LAYER_OPACITY_CONTROL}
198
+ label="Layer opacity"
199
+ value={layer.opacity}
200
+ onchange={(v) => controller.updateAdjustmentLayer(layer.id, { opacity: v })}
201
+ />
202
+
203
+ {#if layer.kind === 'curves' || layer.kind === 'levels'}
204
+ <div class="flex gap-1" role="group" aria-label={layer.kind === 'curves' ? 'Curve channel' : 'Levels channel'}>
205
+ {#each CHANNELS as c (c.id)}
206
+ <button type="button" class={chipClass(channel === c.id)} onclick={() => (channel = c.id)}>{c.label}</button>
207
+ {/each}
208
+ </div>
209
+ {/if}
210
+
211
+ {#if layer.kind === 'curves'}
212
+ <CurvesEditor
213
+ points={layer.params[channel]}
214
+ {channel}
215
+ {histogram}
216
+ onchange={(points: CurvePoint[], commit: boolean) => setParam(layer, channel, points, commit)}
217
+ />
218
+ {:else if layer.kind === 'hueSaturation'}
219
+ <div class="flex flex-wrap gap-1" role="group" aria-label="Hue range">
220
+ <button type="button" class={chipClass(range === -1)} onclick={() => (range = -1)}>Master</button>
221
+ {#each HUE_RANGE_LABELS as name, i (name)}
222
+ <button type="button" class={chipClass(range === i)} disabled={layer.params.colorize} onclick={() => (range = i)}>
223
+ {name}
224
+ </button>
225
+ {/each}
226
+ </div>
227
+ {#if range >= 0 && !layer.params.colorize}
228
+ <svg viewBox="0 0 360 20" preserveAspectRatio="none" class="w-full h-4 rounded" data-testid="hue-range-strip">
229
+ <defs>
230
+ <linearGradient id="pixerate-hue-strip-svelte">
231
+ {#each [0, 60, 120, 180, 240, 300, 360] as h (h)}
232
+ <stop offset="{(h / 360) * 100}%" stop-color="hsl({h}, 90%, 50%)" />
233
+ {/each}
234
+ </linearGradient>
235
+ </defs>
236
+ <rect width="360" height="20" fill="url(#pixerate-hue-strip-svelte)" opacity="0.6" />
237
+ <polyline points={stripPoints(layer.params.ranges[range].bounds)} fill="none" stroke="#fff" stroke-width="1.5" />
238
+ </svg>
239
+ {/if}
240
+ {:else if layer.kind === 'colorBalance'}
241
+ <div class="flex gap-1" role="group" aria-label="Tone">
242
+ {#each TONES as t (t.id)}
243
+ <button type="button" class={chipClass(tone === t.id)} onclick={() => (tone = t.id)}>{t.label}</button>
244
+ {/each}
245
+ </div>
246
+ {:else if layer.kind === 'photoFilter'}
247
+ <label class="flex items-center gap-2 text-slate-300">
248
+ Filter colour
249
+ <input
250
+ type="color"
251
+ aria-label="Filter colour"
252
+ value={layer.params.color}
253
+ oninput={(e) => setParam(layer, 'color', (e.target as HTMLInputElement).value)}
254
+ class="h-5 w-8 bg-transparent"
255
+ />
256
+ </label>
257
+ {:else if layer.kind === 'invert'}
258
+ <p class="text-slate-500 text-[11px]">Invert has no settings.</p>
259
+ {/if}
260
+
261
+ {#each visibleControls(layer) as spec (spec.id)}
262
+ <ControlSlider {spec} value={paramValue(layer, spec)} onchange={(v) => setParam(layer, spec.id, v)} />
263
+ {/each}
264
+
265
+ {#if layer.kind === 'hueSaturation' && range === -1}
266
+ <label class="flex items-center gap-2 text-slate-300">
267
+ <input
268
+ type="checkbox"
269
+ checked={layer.params.colorize}
270
+ onchange={(e) => setParam(layer, 'colorize', (e.target as HTMLInputElement).checked)}
271
+ class="accent-indigo-500"
272
+ />
273
+ Colorize
274
+ </label>
275
+ {/if}
276
+ {#if layer.kind === 'colorBalance' || layer.kind === 'photoFilter'}
277
+ <label class="flex items-center gap-2 text-slate-300">
278
+ <input
279
+ type="checkbox"
280
+ checked={layer.params.preserveLuminosity}
281
+ onchange={(e) => setParam(layer, 'preserveLuminosity', (e.target as HTMLInputElement).checked)}
282
+ class="accent-indigo-500"
283
+ />
284
+ Preserve luminosity
285
+ </label>
286
+ {/if}
287
+ {#if layer.kind === 'blackWhite'}
288
+ <label class="flex items-center gap-2 text-slate-300">
289
+ <input
290
+ type="checkbox"
291
+ checked={layer.params.tint !== null}
292
+ onchange={(e) => setParam(layer, 'tint', (e.target as HTMLInputElement).checked ? '#e1d3b3' : null)}
293
+ class="accent-indigo-500"
294
+ />
295
+ Tint
296
+ {#if layer.params.tint}
297
+ <input
298
+ type="color"
299
+ aria-label="Tint colour"
300
+ value={layer.params.tint}
301
+ oninput={(e) => setParam(layer, 'tint', (e.target as HTMLInputElement).value)}
302
+ class="h-5 w-8 bg-transparent"
303
+ />
304
+ {/if}
305
+ </label>
306
+ {/if}
307
+ </div>
308
+ {/if}
309
+ </div>
@@ -0,0 +1,11 @@
1
+ import type { AdjustmentLayer, ImageEditorController, ImageHistogram } from '@pixerate/editor/image-editor';
2
+ interface Props {
3
+ controller: ImageEditorController;
4
+ /** The current layers (`state.adjustments.layers`), bottom to top. Omit to follow the controller. */
5
+ layers?: AdjustmentLayer[];
6
+ histogram?: ImageHistogram | null;
7
+ }
8
+ declare const AdjustmentLayersPanel: import("svelte").Component<Props, {}, "">;
9
+ type AdjustmentLayersPanel = ReturnType<typeof AdjustmentLayersPanel>;
10
+ export default AdjustmentLayersPanel;
11
+ //# sourceMappingURL=AdjustmentLayersPanel.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdjustmentLayersPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/AdjustmentLayersPanel.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAER,eAAe,EAIf,qBAAqB,EACrB,cAAc,EACf,MAAM,+BAA+B,CAAC;AAiBvC,UAAU,KAAK;IACb,UAAU,EAAE,qBAAqB,CAAC;IAClC,qGAAqG;IACrG,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;CACnC;AA0NH,QAAA,MAAM,qBAAqB,2CAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import type { ControlSpec } from '@pixerate/editor/image-editor';
3
+ import { controlTrackBackground, formatControlValue } from '@pixerate/editor/image-editor';
4
+
5
+ interface Props {
6
+ spec: ControlSpec;
7
+ value: number;
8
+ onchange: (value: number) => void;
9
+ disabled?: boolean;
10
+ /** Overrides `spec.label` (also the accessible name). */
11
+ label?: string;
12
+ }
13
+
14
+ let { spec, value, onchange, disabled = false, label }: Props = $props();
15
+
16
+ const name = $derived(label ?? spec.label);
17
+ const track = $derived(controlTrackBackground(spec));
18
+ </script>
19
+
20
+ <div data-control-id={spec.id}>
21
+ <div class="flex justify-between text-slate-400 mb-1">
22
+ <span>{name}</span>
23
+ <span class="font-mono">{formatControlValue(spec, value)}</span>
24
+ </div>
25
+ <input
26
+ type="range"
27
+ min={spec.min}
28
+ max={spec.max}
29
+ step={spec.step}
30
+ aria-label={name}
31
+ {disabled}
32
+ {value}
33
+ oninput={(e) => onchange(Number((e.target as HTMLInputElement).value))}
34
+ class="w-full accent-indigo-500 disabled:opacity-40"
35
+ />
36
+ {#if track}
37
+ <div class="h-0.5 rounded-full opacity-70" style="background: {track}"></div>
38
+ {/if}
39
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { ControlSpec } from '@pixerate/editor/image-editor';
2
+ interface Props {
3
+ spec: ControlSpec;
4
+ value: number;
5
+ onchange: (value: number) => void;
6
+ disabled?: boolean;
7
+ /** Overrides `spec.label` (also the accessible name). */
8
+ label?: string;
9
+ }
10
+ declare const ControlSlider: import("svelte").Component<Props, {}, "">;
11
+ type ControlSlider = ReturnType<typeof ControlSlider>;
12
+ export default ControlSlider;
13
+ //# sourceMappingURL=ControlSlider.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ControlSlider.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/ControlSlider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAI/D,UAAU,KAAK;IACb,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyBH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -0,0 +1,105 @@
1
+ <script lang="ts">
2
+ import type { CurvePoint, ImageHistogram } from '@pixerate/editor/image-editor';
3
+ import {
4
+ boxToCurvePoint,
5
+ curvePointToBox,
6
+ curveSvgPath,
7
+ hitTestCurvePoint,
8
+ histogramPath,
9
+ insertCurvePoint,
10
+ moveCurvePoint,
11
+ removeCurvePoint,
12
+ } from '@pixerate/editor/image-editor';
13
+
14
+ interface Props {
15
+ points: CurvePoint[];
16
+ /** `commit` is true for the first change of a gesture (one undo step), false for the drag moves. */
17
+ onchange: (points: CurvePoint[], commit: boolean) => void;
18
+ histogram?: ImageHistogram | null;
19
+ channel?: 'master' | 'red' | 'green' | 'blue';
20
+ size?: number;
21
+ }
22
+
23
+ let { points, onchange, histogram = null, channel = 'master', size = 200 }: Props = $props();
24
+
25
+ const BOX = 256;
26
+ const STROKE = { master: '#e2e8f0', red: '#ef4444', green: '#22c55e', blue: '#3b82f6' };
27
+
28
+ let svgEl: SVGSVGElement | undefined = $state();
29
+ let drag: number | null = $state(null);
30
+
31
+ function toBox(e: MouseEvent) {
32
+ const rect = svgEl?.getBoundingClientRect();
33
+ if (!rect || !rect.width || !rect.height) return { x: 0, y: 0 };
34
+ return { x: ((e.clientX - rect.left) / rect.width) * BOX, y: ((e.clientY - rect.top) / rect.height) * BOX };
35
+ }
36
+
37
+ function onpointerdown(e: PointerEvent) {
38
+ const { x, y } = toBox(e);
39
+ let index = hitTestCurvePoint(points, x, y, BOX, BOX, 10);
40
+ if (index === -1) {
41
+ const inserted = insertCurvePoint(points, boxToCurvePoint(x, y, BOX, BOX));
42
+ onchange(inserted.points, true);
43
+ index = inserted.index;
44
+ } else {
45
+ onchange(points, true);
46
+ }
47
+ drag = index;
48
+ (e.target as Element).setPointerCapture?.(e.pointerId);
49
+ }
50
+
51
+ function onpointermove(e: PointerEvent) {
52
+ if (drag === null) return;
53
+ const { x, y } = toBox(e);
54
+ onchange(moveCurvePoint(points, drag, boxToCurvePoint(x, y, BOX, BOX)), false);
55
+ }
56
+
57
+ function ondblclick(e: MouseEvent) {
58
+ const { x, y } = toBox(e);
59
+ const index = hitTestCurvePoint(points, x, y, BOX, BOX, 10);
60
+ if (index !== -1 && points.length > 2) onchange(removeCurvePoint(points, index), true);
61
+ }
62
+
63
+ const histChannel = $derived(channel === 'master' ? 'luminance' : channel);
64
+ </script>
65
+
66
+ <!-- Curves editor over the histogram: click to add a point, drag to move it, double-click to remove it. -->
67
+ <svg
68
+ bind:this={svgEl}
69
+ viewBox="0 0 {BOX} {BOX}"
70
+ width={size}
71
+ height={size}
72
+ role="application"
73
+ aria-label="{channel} curve"
74
+ data-testid="curves-editor"
75
+ class="rounded bg-slate-950/70 touch-none select-none cursor-crosshair"
76
+ {onpointerdown}
77
+ {onpointermove}
78
+ onpointerup={() => (drag = null)}
79
+ onpointercancel={() => (drag = null)}
80
+ {ondblclick}
81
+ >
82
+ {#if histogram && histogram.samples > 0}
83
+ <path d={histogramPath(histogram, histChannel, BOX, BOX)} fill="rgba(148, 163, 184, 0.25)" />
84
+ {/if}
85
+ {#each [64, 128, 192] as v (v)}
86
+ <g stroke="rgba(148, 163, 184, 0.2)" stroke-width="1">
87
+ <line x1={v} y1="0" x2={v} y2={BOX} />
88
+ <line x1="0" y1={v} x2={BOX} y2={v} />
89
+ </g>
90
+ {/each}
91
+ <line x1="0" y1={BOX} x2={BOX} y2="0" stroke="rgba(148, 163, 184, 0.35)" stroke-dasharray="4 4" />
92
+ <path d={curveSvgPath(points, BOX, BOX)} fill="none" stroke={STROKE[channel]} stroke-width="2" />
93
+ {#each points as p, i (i)}
94
+ {@const b = curvePointToBox(p, BOX, BOX)}
95
+ <circle
96
+ cx={b.x}
97
+ cy={b.y}
98
+ r="5"
99
+ fill={drag === i ? STROKE[channel] : '#0f172a'}
100
+ stroke={STROKE[channel]}
101
+ stroke-width="2"
102
+ aria-label="Point {p.input} to {p.output}"
103
+ />
104
+ {/each}
105
+ </svg>
@@ -0,0 +1,13 @@
1
+ import type { CurvePoint, ImageHistogram } from '@pixerate/editor/image-editor';
2
+ interface Props {
3
+ points: CurvePoint[];
4
+ /** `commit` is true for the first change of a gesture (one undo step), false for the drag moves. */
5
+ onchange: (points: CurvePoint[], commit: boolean) => void;
6
+ histogram?: ImageHistogram | null;
7
+ channel?: 'master' | 'red' | 'green' | 'blue';
8
+ size?: number;
9
+ }
10
+ declare const CurvesEditor: import("svelte").Component<Props, {}, "">;
11
+ type CurvesEditor = ReturnType<typeof CurvesEditor>;
12
+ export default CurvesEditor;
13
+ //# sourceMappingURL=CurvesEditor.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CurvesEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/CurvesEditor.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAa9E,UAAU,KAAK;IACb,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,oGAAoG;IACpG,QAAQ,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAC1D,SAAS,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IAClC,OAAO,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAsEH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import type { ImageHistogram } from '@pixerate/editor/image-editor';
3
+ import { histogramPath } from '@pixerate/editor/image-editor';
4
+
5
+ interface Props {
6
+ histogram: ImageHistogram | null;
7
+ /** `rgb` overlays the three channels on the luminance; a single channel draws just that one. */
8
+ channel?: 'rgb' | 'luminance' | 'red' | 'green' | 'blue';
9
+ height?: number;
10
+ class?: string;
11
+ }
12
+
13
+ let { histogram, channel = 'rgb', height = 64, class: className = '' }: Props = $props();
14
+
15
+ const W = 256;
16
+ const FILL: Record<'luminance' | 'red' | 'green' | 'blue', string> = {
17
+ luminance: 'rgba(148, 163, 184, 0.45)',
18
+ red: 'rgba(239, 68, 68, 0.45)',
19
+ green: 'rgba(34, 197, 94, 0.45)',
20
+ blue: 'rgba(59, 130, 246, 0.45)',
21
+ };
22
+
23
+ const channels = $derived(channel === 'rgb' ? (['luminance', 'red', 'green', 'blue'] as const) : ([channel] as const));
24
+ const paths = $derived(
25
+ histogram && histogram.samples > 0 ? channels.map((c) => ({ c, d: histogramPath(histogram, c, W, height) })) : []
26
+ );
27
+ const clipLow = $derived(histogram && histogram.samples ? Math.max(...histogram.shadows) / histogram.samples : 0);
28
+ const clipHigh = $derived(histogram && histogram.samples ? Math.max(...histogram.highlights) / histogram.samples : 0);
29
+ </script>
30
+
31
+ <svg
32
+ viewBox="0 0 {W} {height}"
33
+ preserveAspectRatio="none"
34
+ role="img"
35
+ aria-label={histogram ? `Histogram of ${histogram.samples} pixels` : 'Histogram (no data)'}
36
+ data-testid="image-editor-histogram"
37
+ class="w-full rounded bg-slate-950/60 {className}"
38
+ style="height: {height}px"
39
+ >
40
+ {#each paths as p (p.c)}
41
+ <path d={p.d} fill={FILL[p.c]} stroke="none" />
42
+ {/each}
43
+ {#if clipLow > 0.005}
44
+ <rect x="0" y="0" width="3" {height} fill="#3b82f6" data-testid="histogram-clip-shadows" />
45
+ {/if}
46
+ {#if clipHigh > 0.005}
47
+ <rect x={W - 3} y="0" width="3" {height} fill="#ef4444" data-testid="histogram-clip-highlights" />
48
+ {/if}
49
+ </svg>
@@ -0,0 +1,12 @@
1
+ import type { ImageHistogram } from '@pixerate/editor/image-editor';
2
+ interface Props {
3
+ histogram: ImageHistogram | null;
4
+ /** `rgb` overlays the three channels on the luminance; a single channel draws just that one. */
5
+ channel?: 'rgb' | 'luminance' | 'red' | 'green' | 'blue';
6
+ height?: number;
7
+ class?: string;
8
+ }
9
+ declare const Histogram: import("svelte").Component<Props, {}, "">;
10
+ type Histogram = ReturnType<typeof Histogram>;
11
+ export default Histogram;
12
+ //# sourceMappingURL=Histogram.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Histogram.svelte.d.ts","sourceRoot":"","sources":["../../src/image-editor/Histogram.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAIlE,UAAU,KAAK;IACb,SAAS,EAAE,cAAc,GAAG,IAAI,CAAC;IACjC,gGAAgG;IAChG,OAAO,CAAC,EAAE,KAAK,GAAG,WAAW,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsCH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}