ai-progress-controls 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +823 -0
- package/dist/ai-progress-controls.es.js +7191 -0
- package/dist/ai-progress-controls.es.js.map +1 -0
- package/dist/ai-progress-controls.umd.js +2 -0
- package/dist/ai-progress-controls.umd.js.map +1 -0
- package/dist/index.d.ts +2212 -0
- package/package.json +105 -0
- package/src/__tests__/setup.ts +93 -0
- package/src/core/base/AIControl.ts +230 -0
- package/src/core/base/index.ts +3 -0
- package/src/core/base/types.ts +77 -0
- package/src/core/base/utils.ts +168 -0
- package/src/core/batch-progress/BatchProgress.test.ts +458 -0
- package/src/core/batch-progress/BatchProgress.ts +760 -0
- package/src/core/batch-progress/index.ts +14 -0
- package/src/core/batch-progress/styles.ts +480 -0
- package/src/core/batch-progress/types.ts +169 -0
- package/src/core/model-loader/ModelLoader.test.ts +311 -0
- package/src/core/model-loader/ModelLoader.ts +673 -0
- package/src/core/model-loader/index.ts +2 -0
- package/src/core/model-loader/styles.ts +496 -0
- package/src/core/model-loader/types.ts +127 -0
- package/src/core/parameter-panel/ParameterPanel.test.ts +856 -0
- package/src/core/parameter-panel/ParameterPanel.ts +877 -0
- package/src/core/parameter-panel/index.ts +14 -0
- package/src/core/parameter-panel/styles.ts +323 -0
- package/src/core/parameter-panel/types.ts +278 -0
- package/src/core/parameter-slider/ParameterSlider.test.ts +299 -0
- package/src/core/parameter-slider/ParameterSlider.ts +653 -0
- package/src/core/parameter-slider/index.ts +8 -0
- package/src/core/parameter-slider/styles.ts +493 -0
- package/src/core/parameter-slider/types.ts +107 -0
- package/src/core/queue-progress/QueueProgress.test.ts +344 -0
- package/src/core/queue-progress/QueueProgress.ts +563 -0
- package/src/core/queue-progress/index.ts +5 -0
- package/src/core/queue-progress/styles.ts +469 -0
- package/src/core/queue-progress/types.ts +130 -0
- package/src/core/retry-progress/RetryProgress.test.ts +397 -0
- package/src/core/retry-progress/RetryProgress.ts +957 -0
- package/src/core/retry-progress/index.ts +6 -0
- package/src/core/retry-progress/styles.ts +530 -0
- package/src/core/retry-progress/types.ts +176 -0
- package/src/core/stream-progress/StreamProgress.test.ts +531 -0
- package/src/core/stream-progress/StreamProgress.ts +517 -0
- package/src/core/stream-progress/index.ts +2 -0
- package/src/core/stream-progress/styles.ts +349 -0
- package/src/core/stream-progress/types.ts +82 -0
- package/src/index.ts +19 -0
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import { AIControl } from '../base/AIControl';
|
|
2
|
+
import { clamp } from '../base/utils';
|
|
3
|
+
import type {
|
|
4
|
+
ParameterSliderConfig,
|
|
5
|
+
ParameterSliderState,
|
|
6
|
+
PresetValue,
|
|
7
|
+
ValueChangeEvent,
|
|
8
|
+
PresetSelectEvent,
|
|
9
|
+
} from './types';
|
|
10
|
+
import { styles } from './styles';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ParameterSlider Component
|
|
14
|
+
*
|
|
15
|
+
* Interactive slider for AI parameter configuration (temperature, top-p, penalties, etc.).
|
|
16
|
+
* Supports presets, manual input, keyboard navigation, and accessibility.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* // Create temperature slider
|
|
21
|
+
* const slider = new ParameterSlider({
|
|
22
|
+
* label: 'Temperature',
|
|
23
|
+
* min: 0,
|
|
24
|
+
* max: 2,
|
|
25
|
+
* value: 0.7,
|
|
26
|
+
* step: 0.1,
|
|
27
|
+
* description: 'Controls randomness in responses',
|
|
28
|
+
* presets: [
|
|
29
|
+
* { value: 0, label: 'Focused' },
|
|
30
|
+
* { value: 0.7, label: 'Balanced' },
|
|
31
|
+
* { value: 1.5, label: 'Creative' }
|
|
32
|
+
* ]
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* document.body.appendChild(slider);
|
|
36
|
+
*
|
|
37
|
+
* // Listen to value changes
|
|
38
|
+
* slider.addEventListener('valuechange', (e) => {
|
|
39
|
+
* console.log('New value:', e.detail.value);
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* // Get current value
|
|
43
|
+
* const value = slider.getValue();
|
|
44
|
+
*
|
|
45
|
+
* // Set value programmatically
|
|
46
|
+
* slider.setValue(1.2);
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @fires valuechange - Fired when value changes
|
|
50
|
+
* @fires presetselect - Fired when preset is selected
|
|
51
|
+
*/
|
|
52
|
+
export class ParameterSlider extends AIControl {
|
|
53
|
+
protected override config: Required<ParameterSliderConfig>;
|
|
54
|
+
private readonly state: ParameterSliderState;
|
|
55
|
+
private sliderTrack: HTMLElement | null = null;
|
|
56
|
+
private sliderThumb: HTMLElement | null = null;
|
|
57
|
+
|
|
58
|
+
static get observedAttributes() {
|
|
59
|
+
return ['label', 'min', 'max', 'value', 'disabled', 'size', 'variant', 'animation'];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
constructor(config: ParameterSliderConfig = {}) {
|
|
63
|
+
super({
|
|
64
|
+
debug: config.debug ?? false,
|
|
65
|
+
className: config.className,
|
|
66
|
+
ariaLabel: config.ariaLabel ?? `${config.label ?? 'Parameter'} Slider`,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Set default configuration
|
|
70
|
+
this.config = {
|
|
71
|
+
label: config.label ?? 'Parameter',
|
|
72
|
+
min: config.min ?? 0,
|
|
73
|
+
max: config.max ?? 1,
|
|
74
|
+
value: config.value ?? 0.5,
|
|
75
|
+
defaultValue: config.defaultValue ?? config.value ?? 0.5,
|
|
76
|
+
step: config.step ?? 0.01,
|
|
77
|
+
decimals: config.decimals ?? 2,
|
|
78
|
+
description: config.description ?? '',
|
|
79
|
+
showPresets: config.showPresets ?? true,
|
|
80
|
+
presets: config.presets ?? [],
|
|
81
|
+
showInput: config.showInput ?? true,
|
|
82
|
+
showReset: config.showReset ?? true,
|
|
83
|
+
showRangeLabels: config.showRangeLabels ?? true,
|
|
84
|
+
unit: config.unit ?? '',
|
|
85
|
+
cursorFeedback: config.cursorFeedback ?? true,
|
|
86
|
+
disabled: config.disabled ?? false,
|
|
87
|
+
debug: config.debug ?? false,
|
|
88
|
+
className: config.className ?? '',
|
|
89
|
+
ariaLabel: config.ariaLabel ?? `${config.label ?? 'Parameter'} Slider`,
|
|
90
|
+
size: config.size ?? 'default',
|
|
91
|
+
variant: config.variant ?? 'default',
|
|
92
|
+
animation: config.animation ?? 'none',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Initialize state
|
|
96
|
+
this.state = {
|
|
97
|
+
currentValue: clamp(this.config.value, this.config.min, this.config.max),
|
|
98
|
+
isDragging: false,
|
|
99
|
+
isFocused: false,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Attach shadow DOM
|
|
103
|
+
this.attachShadow({ mode: 'open' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
override connectedCallback(): void {
|
|
107
|
+
super.connectedCallback();
|
|
108
|
+
this.render();
|
|
109
|
+
this.attachEventListeners();
|
|
110
|
+
this.log('ParameterSlider mounted');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
override disconnectedCallback(): void {
|
|
114
|
+
this.removeEventListeners();
|
|
115
|
+
super.disconnectedCallback();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
protected override getDefaultRole(): string {
|
|
119
|
+
return 'slider';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected override handleAttributeChange(
|
|
123
|
+
name: string,
|
|
124
|
+
_oldValue: string,
|
|
125
|
+
newValue: string
|
|
126
|
+
): void {
|
|
127
|
+
switch (name) {
|
|
128
|
+
case 'label':
|
|
129
|
+
this.config.label = newValue || 'Parameter';
|
|
130
|
+
this.render();
|
|
131
|
+
break;
|
|
132
|
+
case 'min':
|
|
133
|
+
this.config.min = Number.parseFloat(newValue) || 0;
|
|
134
|
+
this.render();
|
|
135
|
+
break;
|
|
136
|
+
case 'max':
|
|
137
|
+
this.config.max = Number.parseFloat(newValue) || 1;
|
|
138
|
+
this.render();
|
|
139
|
+
break;
|
|
140
|
+
case 'value':
|
|
141
|
+
this.setValue(Number.parseFloat(newValue) || 0.5);
|
|
142
|
+
break;
|
|
143
|
+
case 'disabled':
|
|
144
|
+
this._disabled = newValue !== null;
|
|
145
|
+
this.render();
|
|
146
|
+
break;
|
|
147
|
+
case 'size':
|
|
148
|
+
this.config.size = newValue as any;
|
|
149
|
+
this.render();
|
|
150
|
+
break;
|
|
151
|
+
case 'variant':
|
|
152
|
+
this.config.variant = newValue as any;
|
|
153
|
+
this.render();
|
|
154
|
+
break;
|
|
155
|
+
case 'animation':
|
|
156
|
+
this.config.animation = newValue as any;
|
|
157
|
+
this.render();
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get current value
|
|
164
|
+
*/
|
|
165
|
+
public getValue(): number {
|
|
166
|
+
return this.state.currentValue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Set value programmatically
|
|
171
|
+
*/
|
|
172
|
+
public setValue(value: number, source: 'slider' | 'input' | 'preset' | 'reset' = 'slider'): void {
|
|
173
|
+
const previousValue = this.state.currentValue;
|
|
174
|
+
const clampedValue = clamp(value, this.config.min, this.config.max);
|
|
175
|
+
|
|
176
|
+
// Round to step
|
|
177
|
+
const steppedValue = Math.round(clampedValue / this.config.step) * this.config.step;
|
|
178
|
+
const finalValue = Number.parseFloat(steppedValue.toFixed(this.config.decimals));
|
|
179
|
+
|
|
180
|
+
if (finalValue === this.state.currentValue) return;
|
|
181
|
+
|
|
182
|
+
this.state.currentValue = finalValue;
|
|
183
|
+
this.updateSliderPosition();
|
|
184
|
+
|
|
185
|
+
const event: ValueChangeEvent = {
|
|
186
|
+
value: finalValue,
|
|
187
|
+
previousValue,
|
|
188
|
+
source,
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
this.emit('valuechange', event);
|
|
193
|
+
this.log('Value changed', event);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Reset to default value
|
|
198
|
+
*/
|
|
199
|
+
public reset(): void {
|
|
200
|
+
this.setValue(this.config.defaultValue, 'reset');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Select a preset value
|
|
205
|
+
*/
|
|
206
|
+
public selectPreset(preset: PresetValue): void {
|
|
207
|
+
const previousValue = this.state.currentValue;
|
|
208
|
+
this.setValue(preset.value, 'preset');
|
|
209
|
+
|
|
210
|
+
const event: PresetSelectEvent = {
|
|
211
|
+
preset,
|
|
212
|
+
previousValue,
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
this.emit('presetselect', event);
|
|
217
|
+
this.log('Preset selected', event);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Attach event listeners to slider elements
|
|
222
|
+
*/
|
|
223
|
+
private attachEventListeners(): void {
|
|
224
|
+
if (!this.shadowRoot) return;
|
|
225
|
+
|
|
226
|
+
// Slider track and thumb
|
|
227
|
+
this.sliderTrack = this.shadowRoot.querySelector('.slider-track');
|
|
228
|
+
this.sliderThumb = this.shadowRoot.querySelector('.slider-thumb');
|
|
229
|
+
|
|
230
|
+
if (this.sliderTrack) {
|
|
231
|
+
this.sliderTrack.addEventListener('click', this.handleTrackClick.bind(this));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (this.sliderThumb) {
|
|
235
|
+
this.sliderThumb.addEventListener('mousedown', this.handleThumbMouseDown.bind(this));
|
|
236
|
+
this.sliderThumb.addEventListener('touchstart', this.handleThumbTouchStart.bind(this), {
|
|
237
|
+
passive: false,
|
|
238
|
+
});
|
|
239
|
+
this.sliderThumb.addEventListener('keydown', this.handleThumbKeyDown.bind(this));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Value input
|
|
243
|
+
const input = this.shadowRoot.querySelector('.value-input') as HTMLInputElement;
|
|
244
|
+
if (input) {
|
|
245
|
+
input.addEventListener('change', this.handleInputChange.bind(this));
|
|
246
|
+
input.addEventListener('blur', this.handleInputBlur.bind(this));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Reset button
|
|
250
|
+
const resetBtn = this.shadowRoot.querySelector('.reset-button');
|
|
251
|
+
if (resetBtn) {
|
|
252
|
+
resetBtn.addEventListener('click', () => this.reset());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Preset buttons
|
|
256
|
+
const presetButtons = this.shadowRoot.querySelectorAll('.preset-button');
|
|
257
|
+
presetButtons.forEach((btn, index) => {
|
|
258
|
+
btn.addEventListener('click', () => {
|
|
259
|
+
const preset = this.config.presets[index];
|
|
260
|
+
if (preset) this.selectPreset(preset);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Remove event listeners
|
|
267
|
+
*/
|
|
268
|
+
private removeEventListeners(): void {
|
|
269
|
+
document.removeEventListener('mousemove', this.handleThumbMouseMove);
|
|
270
|
+
document.removeEventListener('mouseup', this.handleThumbMouseUp);
|
|
271
|
+
document.removeEventListener('touchmove', this.handleThumbTouchMove);
|
|
272
|
+
document.removeEventListener('touchend', this.handleThumbTouchEnd);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Handle track click
|
|
277
|
+
*/
|
|
278
|
+
private handleTrackClick(e: MouseEvent): void {
|
|
279
|
+
if (!this.sliderTrack || this.config.disabled) return;
|
|
280
|
+
|
|
281
|
+
const rect = this.sliderTrack.getBoundingClientRect();
|
|
282
|
+
const percent = (e.clientX - rect.left) / rect.width;
|
|
283
|
+
const value = this.config.min + percent * (this.config.max - this.config.min);
|
|
284
|
+
|
|
285
|
+
this.setValue(value, 'slider');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Handle thumb mouse down
|
|
290
|
+
*/
|
|
291
|
+
private handleThumbMouseDown(e: MouseEvent): void {
|
|
292
|
+
if (this.config.disabled) return;
|
|
293
|
+
|
|
294
|
+
e.preventDefault();
|
|
295
|
+
this.state.isDragging = true;
|
|
296
|
+
this.sliderThumb?.classList.add('dragging');
|
|
297
|
+
|
|
298
|
+
document.addEventListener('mousemove', this.handleThumbMouseMove);
|
|
299
|
+
document.addEventListener('mouseup', this.handleThumbMouseUp);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Handle thumb mouse move
|
|
304
|
+
*/
|
|
305
|
+
private readonly handleThumbMouseMove = (e: MouseEvent): void => {
|
|
306
|
+
if (!this.state.isDragging || !this.sliderTrack) return;
|
|
307
|
+
|
|
308
|
+
const rect = this.sliderTrack.getBoundingClientRect();
|
|
309
|
+
const percent = clamp((e.clientX - rect.left) / rect.width, 0, 1);
|
|
310
|
+
const value = this.config.min + percent * (this.config.max - this.config.min);
|
|
311
|
+
|
|
312
|
+
this.setValue(value, 'slider');
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Handle thumb mouse up
|
|
317
|
+
*/
|
|
318
|
+
private readonly handleThumbMouseUp = (): void => {
|
|
319
|
+
this.state.isDragging = false;
|
|
320
|
+
this.sliderThumb?.classList.remove('dragging');
|
|
321
|
+
|
|
322
|
+
document.removeEventListener('mousemove', this.handleThumbMouseMove);
|
|
323
|
+
document.removeEventListener('mouseup', this.handleThumbMouseUp);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Handle thumb touch start
|
|
328
|
+
*/
|
|
329
|
+
private handleThumbTouchStart(e: TouchEvent): void {
|
|
330
|
+
if (this.config.disabled) return;
|
|
331
|
+
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
this.state.isDragging = true;
|
|
334
|
+
this.sliderThumb?.classList.add('dragging');
|
|
335
|
+
|
|
336
|
+
document.addEventListener('touchmove', this.handleThumbTouchMove, { passive: false });
|
|
337
|
+
document.addEventListener('touchend', this.handleThumbTouchEnd);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Handle thumb touch move
|
|
342
|
+
*/
|
|
343
|
+
private readonly handleThumbTouchMove = (e: TouchEvent): void => {
|
|
344
|
+
if (!this.state.isDragging || !this.sliderTrack) return;
|
|
345
|
+
|
|
346
|
+
e.preventDefault();
|
|
347
|
+
const touch = e.touches[0];
|
|
348
|
+
if (!touch) return;
|
|
349
|
+
|
|
350
|
+
const rect = this.sliderTrack.getBoundingClientRect();
|
|
351
|
+
const percent = clamp((touch.clientX - rect.left) / rect.width, 0, 1);
|
|
352
|
+
const value = this.config.min + percent * (this.config.max - this.config.min);
|
|
353
|
+
|
|
354
|
+
this.setValue(value, 'slider');
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Handle thumb touch end
|
|
359
|
+
*/
|
|
360
|
+
private readonly handleThumbTouchEnd = (): void => {
|
|
361
|
+
this.state.isDragging = false;
|
|
362
|
+
this.sliderThumb?.classList.remove('dragging');
|
|
363
|
+
|
|
364
|
+
document.removeEventListener('touchmove', this.handleThumbTouchMove);
|
|
365
|
+
document.removeEventListener('touchend', this.handleThumbTouchEnd);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Handle keyboard navigation
|
|
370
|
+
*/
|
|
371
|
+
private handleThumbKeyDown(e: KeyboardEvent): void {
|
|
372
|
+
if (this.config.disabled) return;
|
|
373
|
+
|
|
374
|
+
let delta = 0;
|
|
375
|
+
const largeStep = this.config.step * 10;
|
|
376
|
+
|
|
377
|
+
switch (e.key) {
|
|
378
|
+
case 'ArrowRight':
|
|
379
|
+
case 'ArrowUp':
|
|
380
|
+
delta = this.config.step;
|
|
381
|
+
break;
|
|
382
|
+
case 'ArrowLeft':
|
|
383
|
+
case 'ArrowDown':
|
|
384
|
+
delta = -this.config.step;
|
|
385
|
+
break;
|
|
386
|
+
case 'PageUp':
|
|
387
|
+
delta = largeStep;
|
|
388
|
+
break;
|
|
389
|
+
case 'PageDown':
|
|
390
|
+
delta = -largeStep;
|
|
391
|
+
break;
|
|
392
|
+
case 'Home':
|
|
393
|
+
this.setValue(this.config.min, 'slider');
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
return;
|
|
396
|
+
case 'End':
|
|
397
|
+
this.setValue(this.config.max, 'slider');
|
|
398
|
+
e.preventDefault();
|
|
399
|
+
return;
|
|
400
|
+
default:
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
e.preventDefault();
|
|
405
|
+
this.setValue(this.state.currentValue + delta, 'slider');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Handle input field change
|
|
410
|
+
*/
|
|
411
|
+
private handleInputChange(e: Event): void {
|
|
412
|
+
const input = e.target as HTMLInputElement;
|
|
413
|
+
const value = Number.parseFloat(input.value);
|
|
414
|
+
|
|
415
|
+
if (!Number.isNaN(value)) {
|
|
416
|
+
this.setValue(value, 'input');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Handle input field blur (validate)
|
|
422
|
+
*/
|
|
423
|
+
private handleInputBlur(e: Event): void {
|
|
424
|
+
const input = e.target as HTMLInputElement;
|
|
425
|
+
input.value = this.state.currentValue.toFixed(this.config.decimals);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Update slider thumb position based on current value
|
|
430
|
+
*/
|
|
431
|
+
private updateSliderPosition(): void {
|
|
432
|
+
if (!this.sliderThumb || !this.shadowRoot) return;
|
|
433
|
+
|
|
434
|
+
const percent =
|
|
435
|
+
((this.state.currentValue - this.config.min) / (this.config.max - this.config.min)) * 100;
|
|
436
|
+
this.sliderThumb.style.left = `${percent}%`;
|
|
437
|
+
|
|
438
|
+
const fill = this.shadowRoot.querySelector('.slider-fill') as HTMLElement;
|
|
439
|
+
if (fill) {
|
|
440
|
+
fill.style.width = `${percent}%`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Update value display
|
|
444
|
+
const valueDisplay = this.shadowRoot.querySelector('.current-value');
|
|
445
|
+
if (valueDisplay) {
|
|
446
|
+
valueDisplay.textContent = this.state.currentValue.toFixed(this.config.decimals);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Update input field
|
|
450
|
+
const input = this.shadowRoot.querySelector('.value-input') as HTMLInputElement;
|
|
451
|
+
if (input && document.activeElement !== input) {
|
|
452
|
+
input.value = this.state.currentValue.toFixed(this.config.decimals);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Update active preset
|
|
456
|
+
this.updateActivePreset();
|
|
457
|
+
this.updateCursor();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Update cursor based on slider state
|
|
462
|
+
*/
|
|
463
|
+
private updateCursor(): void {
|
|
464
|
+
if (!this.config.cursorFeedback || !this.sliderThumb) return;
|
|
465
|
+
|
|
466
|
+
if (this.config.disabled) {
|
|
467
|
+
this.sliderThumb.style.cursor = 'not-allowed';
|
|
468
|
+
} else if (this.state.isDragging) {
|
|
469
|
+
this.sliderThumb.style.cursor = 'grabbing';
|
|
470
|
+
} else {
|
|
471
|
+
this.sliderThumb.style.cursor = 'grab';
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Update active preset button
|
|
477
|
+
*/
|
|
478
|
+
private updateActivePreset(): void {
|
|
479
|
+
if (!this.shadowRoot) return;
|
|
480
|
+
|
|
481
|
+
const presetButtons = this.shadowRoot.querySelectorAll('.preset-button');
|
|
482
|
+
presetButtons.forEach((btn, index) => {
|
|
483
|
+
const preset = this.config.presets[index];
|
|
484
|
+
if (preset && Math.abs(preset.value - this.state.currentValue) < this.config.step / 2) {
|
|
485
|
+
btn.classList.add('active');
|
|
486
|
+
} else {
|
|
487
|
+
btn.classList.remove('active');
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Render input controls section
|
|
494
|
+
*/
|
|
495
|
+
private renderInputControls(): string {
|
|
496
|
+
if (!this.config.showInput && !this.config.showReset) return '';
|
|
497
|
+
|
|
498
|
+
const disabledAttr = this.config.disabled ? 'disabled' : '';
|
|
499
|
+
let controlsHtml = '';
|
|
500
|
+
|
|
501
|
+
if (this.config.showInput) {
|
|
502
|
+
controlsHtml += `
|
|
503
|
+
<span class="input-label">Value:</span>
|
|
504
|
+
<input
|
|
505
|
+
type="number"
|
|
506
|
+
class="value-input"
|
|
507
|
+
value="${this.state.currentValue.toFixed(this.config.decimals)}"
|
|
508
|
+
min="${this.config.min}"
|
|
509
|
+
max="${this.config.max}"
|
|
510
|
+
step="${this.config.step}"
|
|
511
|
+
${disabledAttr}
|
|
512
|
+
/>
|
|
513
|
+
`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (this.config.showReset) {
|
|
517
|
+
controlsHtml += `
|
|
518
|
+
<button class="reset-button" type="button" ${disabledAttr}>
|
|
519
|
+
Reset
|
|
520
|
+
</button>
|
|
521
|
+
`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return `
|
|
525
|
+
<div class="input-container">
|
|
526
|
+
${controlsHtml}
|
|
527
|
+
</div>
|
|
528
|
+
`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Render the component
|
|
533
|
+
*/
|
|
534
|
+
protected render(): void {
|
|
535
|
+
if (!this.shadowRoot) return;
|
|
536
|
+
|
|
537
|
+
// Sync attributes to host element for CSS selectors
|
|
538
|
+
if (this.config.size && this.getAttribute('size') !== this.config.size) {
|
|
539
|
+
this.setAttribute('size', this.config.size);
|
|
540
|
+
}
|
|
541
|
+
if (this.config.variant && this.getAttribute('variant') !== this.config.variant) {
|
|
542
|
+
this.setAttribute('variant', this.config.variant);
|
|
543
|
+
}
|
|
544
|
+
if (this.config.animation && this.getAttribute('animation') !== this.config.animation) {
|
|
545
|
+
this.setAttribute('animation', this.config.animation);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const disabledClass = this.config.disabled ? 'disabled' : '';
|
|
549
|
+
|
|
550
|
+
// Header with label and current value
|
|
551
|
+
const headerHtml = `
|
|
552
|
+
<div class="header">
|
|
553
|
+
<div class="label-section">
|
|
554
|
+
<label class="label">${this.config.label}</label>
|
|
555
|
+
${this.config.description ? `<div class="description">${this.config.description}</div>` : ''}
|
|
556
|
+
</div>
|
|
557
|
+
<div class="value-display">
|
|
558
|
+
<span class="current-value">${this.state.currentValue.toFixed(this.config.decimals)}</span>
|
|
559
|
+
${this.config.unit ? `<span class="unit">${this.config.unit}</span>` : ''}
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
`;
|
|
563
|
+
|
|
564
|
+
// Slider
|
|
565
|
+
const percent =
|
|
566
|
+
((this.state.currentValue - this.config.min) / (this.config.max - this.config.min)) * 100;
|
|
567
|
+
const sliderHtml = `
|
|
568
|
+
<div class="slider-container">
|
|
569
|
+
<div class="slider-track" role="presentation">
|
|
570
|
+
<div class="slider-fill" style="width: ${percent}%"></div>
|
|
571
|
+
<div
|
|
572
|
+
class="slider-thumb"
|
|
573
|
+
style="left: ${percent}%"
|
|
574
|
+
role="slider"
|
|
575
|
+
tabindex="${this.config.disabled ? -1 : 0}"
|
|
576
|
+
aria-valuemin="${this.config.min}"
|
|
577
|
+
aria-valuemax="${this.config.max}"
|
|
578
|
+
aria-valuenow="${this.state.currentValue}"
|
|
579
|
+
aria-label="${this.config.ariaLabel}"
|
|
580
|
+
></div>
|
|
581
|
+
</div>
|
|
582
|
+
${
|
|
583
|
+
this.config.showRangeLabels
|
|
584
|
+
? `
|
|
585
|
+
<div class="range-labels">
|
|
586
|
+
<span class="range-label">${this.config.min.toFixed(this.config.decimals)}</span>
|
|
587
|
+
<span class="range-label">${this.config.max.toFixed(this.config.decimals)}</span>
|
|
588
|
+
</div>
|
|
589
|
+
`
|
|
590
|
+
: ''
|
|
591
|
+
}
|
|
592
|
+
</div>
|
|
593
|
+
`;
|
|
594
|
+
|
|
595
|
+
// Presets
|
|
596
|
+
const presetsHtml =
|
|
597
|
+
this.config.showPresets && this.config.presets.length > 0
|
|
598
|
+
? `
|
|
599
|
+
<div class="presets">
|
|
600
|
+
${this.config.presets
|
|
601
|
+
.map(
|
|
602
|
+
(preset) => `
|
|
603
|
+
<button class="preset-button" type="button">
|
|
604
|
+
<span class="preset-value">${preset.value}</span>
|
|
605
|
+
<span class="preset-label">${preset.label}</span>
|
|
606
|
+
</button>
|
|
607
|
+
`
|
|
608
|
+
)
|
|
609
|
+
.join('')}
|
|
610
|
+
</div>
|
|
611
|
+
`
|
|
612
|
+
: '';
|
|
613
|
+
|
|
614
|
+
// Input and reset
|
|
615
|
+
const inputHtml = this.renderInputControls();
|
|
616
|
+
|
|
617
|
+
this.shadowRoot.innerHTML = `
|
|
618
|
+
${styles}
|
|
619
|
+
<div class="parameter-slider ${disabledClass} ${this.config.className}">
|
|
620
|
+
${headerHtml}
|
|
621
|
+
${sliderHtml}
|
|
622
|
+
${presetsHtml}
|
|
623
|
+
${inputHtml}
|
|
624
|
+
</div>
|
|
625
|
+
`;
|
|
626
|
+
|
|
627
|
+
// Store references after render
|
|
628
|
+
this.sliderTrack = this.shadowRoot.querySelector('.slider-track');
|
|
629
|
+
this.sliderThumb = this.shadowRoot.querySelector('.slider-thumb');
|
|
630
|
+
|
|
631
|
+
// Reattach event listeners
|
|
632
|
+
this.attachEventListeners();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Get current state (for debugging)
|
|
637
|
+
*/
|
|
638
|
+
public getState(): Readonly<ParameterSliderState> {
|
|
639
|
+
return { ...this.state };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Get current configuration
|
|
644
|
+
*/
|
|
645
|
+
public getConfig(): Readonly<Required<ParameterSliderConfig>> {
|
|
646
|
+
return { ...this.config };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Register the custom element
|
|
651
|
+
if (!customElements.get('parameter-slider')) {
|
|
652
|
+
customElements.define('parameter-slider', ParameterSlider);
|
|
653
|
+
}
|