mtrl 0.2.8 → 0.2.9

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 (42) hide show
  1. package/index.ts +2 -0
  2. package/package.json +1 -1
  3. package/src/components/navigation/api.ts +131 -96
  4. package/src/components/navigation/features/controller.ts +273 -0
  5. package/src/components/navigation/features/items.ts +133 -64
  6. package/src/components/navigation/navigation.ts +17 -2
  7. package/src/components/navigation/system-types.ts +124 -0
  8. package/src/components/navigation/system.ts +776 -0
  9. package/src/components/slider/config.ts +20 -2
  10. package/src/components/slider/features/controller.ts +761 -0
  11. package/src/components/slider/features/handlers.ts +18 -15
  12. package/src/components/slider/features/index.ts +3 -2
  13. package/src/components/slider/features/range.ts +104 -0
  14. package/src/components/slider/slider.ts +34 -14
  15. package/src/components/slider/structure.ts +152 -0
  16. package/src/components/textfield/api.ts +53 -0
  17. package/src/components/textfield/features.ts +322 -0
  18. package/src/components/textfield/textfield.ts +8 -0
  19. package/src/components/textfield/types.ts +12 -3
  20. package/src/components/timepicker/clockdial.ts +1 -4
  21. package/src/core/compose/features/textinput.ts +15 -2
  22. package/src/core/composition/features/dom.ts +33 -0
  23. package/src/core/composition/features/icon.ts +131 -0
  24. package/src/core/composition/features/index.ts +11 -0
  25. package/src/core/composition/features/label.ts +156 -0
  26. package/src/core/composition/features/structure.ts +22 -0
  27. package/src/core/composition/index.ts +26 -0
  28. package/src/core/index.ts +1 -1
  29. package/src/core/structure.ts +288 -0
  30. package/src/index.ts +1 -0
  31. package/src/styles/components/_navigation-mobile.scss +244 -0
  32. package/src/styles/components/_navigation-system.scss +151 -0
  33. package/src/styles/components/_textfield.scss +250 -11
  34. package/demo/build.ts +0 -349
  35. package/demo/index.html +0 -110
  36. package/demo/main.js +0 -448
  37. package/demo/styles.css +0 -239
  38. package/server.ts +0 -86
  39. package/src/components/slider/features/slider.ts +0 -318
  40. package/src/components/slider/features/structure.ts +0 -181
  41. package/src/components/slider/features/ui.ts +0 -388
  42. package/src/components/textfield/constants.ts +0 -100
@@ -0,0 +1,761 @@
1
+ // src/components/slider/features/controller.ts
2
+ import { SLIDER_EVENTS } from '../constants';
3
+ import { SliderConfig } from '../types';
4
+ import { createHandlers } from './handlers';
5
+
6
+ /**
7
+ * Add controller functionality to slider component
8
+ * Manages state, events, user interactions, and UI rendering
9
+ *
10
+ * @param config Slider configuration
11
+ * @returns Component enhancer with slider controller functionality
12
+ */
13
+ export const withController = (config: SliderConfig) => component => {
14
+ // Ensure component has required properties
15
+ if (!component.element || !component.components) {
16
+ console.warn('Cannot initialize slider controller: missing element or components');
17
+ return component;
18
+ }
19
+
20
+ // Ensure component has events
21
+ if (!component.events) {
22
+ component.events = {
23
+ listeners: {},
24
+ on(event, handler) {
25
+ if (!this.listeners[event]) this.listeners[event] = [];
26
+ this.listeners[event].push(handler);
27
+ return this;
28
+ },
29
+ off(event, handler) {
30
+ if (this.listeners[event]) {
31
+ this.listeners[event] = this.listeners[event].filter(h => h !== handler);
32
+ }
33
+ return this;
34
+ },
35
+ trigger(event, data) {
36
+ if (this.listeners[event]) {
37
+ this.listeners[event].forEach(handler => handler(data));
38
+ }
39
+ return this;
40
+ }
41
+ };
42
+ }
43
+
44
+ // Initialize state
45
+ const state = {
46
+ value: config.value !== undefined ? config.value : 0,
47
+ secondValue: config.secondValue !== undefined ? config.secondValue : null,
48
+ min: config.min !== undefined ? config.min : 0,
49
+ max: config.max !== undefined ? config.max : 100,
50
+ step: config.step !== undefined ? config.step : 1,
51
+ dragging: false,
52
+ activeBubble: null,
53
+ activeHandle: null,
54
+ ticks: [],
55
+ valueHideTimer: null,
56
+ component
57
+ };
58
+
59
+ // Create event helpers
60
+ const eventHelpers = {
61
+ triggerEvent(eventName, originalEvent = null) {
62
+ const eventData = {
63
+ slider: state.component,
64
+ value: state.value,
65
+ secondValue: state.secondValue,
66
+ originalEvent,
67
+ preventDefault: () => { eventData.defaultPrevented = true; },
68
+ defaultPrevented: false
69
+ };
70
+
71
+ state.component.events.trigger(eventName, eventData);
72
+ return eventData;
73
+ }
74
+ };
75
+
76
+ //=============================================================================
77
+ // UI RENDERING FUNCTIONS
78
+ //=============================================================================
79
+
80
+ /**
81
+ * Gets required components from state, safely handling missing components
82
+ */
83
+ const getComponents = () => {
84
+ // Return empty object if component or components are missing
85
+ if (!state.component?.components) {
86
+ return {};
87
+ }
88
+
89
+ // Get flattened components
90
+ return state.component.components;
91
+ };
92
+
93
+ /**
94
+ * Calculates percentage position for a value
95
+ * Maps from value space (min-max) to percentage space (0-100)
96
+ */
97
+ const getPercentage = (value) => {
98
+ const range = state.max - state.min;
99
+ return range === 0 ? 0 : ((value - state.min) / range) * 100;
100
+ };
101
+
102
+ /**
103
+ * Gets track dimensions and constraints for positioning calculations
104
+ * Handles edge constraints and padding for proper handle positioning
105
+ */
106
+ const getTrackDimensions = () => {
107
+ const components = getComponents();
108
+ const { handle, container, track } = components;
109
+
110
+ if (!handle || !container || !track) return null;
111
+
112
+ try {
113
+ const handleRect = handle.getBoundingClientRect();
114
+ const trackRect = container.getBoundingClientRect();
115
+ const handleSize = handleRect.width || 20;
116
+ const trackSize = trackRect.width;
117
+
118
+ const edgeConstraint = (handleSize / 2) / trackSize * 100;
119
+ const paddingPixels = state.activeHandle ? 6 : 8;
120
+ const paddingPercent = (paddingPixels / trackSize) * 100;
121
+
122
+ return { handleSize, trackSize, edgeConstraint, paddingPercent };
123
+ } catch (error) {
124
+ console.warn('Error calculating track dimensions:', error);
125
+ return {
126
+ handleSize: 20,
127
+ trackSize: 200,
128
+ edgeConstraint: 5,
129
+ paddingPercent: 4
130
+ };
131
+ }
132
+ };
133
+
134
+ /**
135
+ * Maps value percentage to visual position with edge constraints
136
+ * Ensures handles stay within the visible track area
137
+ */
138
+ const mapValueToVisualPercent = (valuePercent, edgeConstraint) => {
139
+ const minEdge = edgeConstraint;
140
+ const maxEdge = 100 - edgeConstraint;
141
+ const visualRange = maxEdge - minEdge;
142
+
143
+ if (valuePercent <= 0) return minEdge;
144
+ if (valuePercent >= 100) return maxEdge;
145
+ return minEdge + (valuePercent / 100) * visualRange;
146
+ };
147
+
148
+ /**
149
+ * Gets slider value from a position on the track
150
+ * Maps from pixel position to slider value
151
+ */
152
+ const getValueFromPosition = (position) => {
153
+ const components = getComponents();
154
+ const { handle, container } = components;
155
+
156
+ if (!handle || !container) return state.min;
157
+
158
+ try {
159
+ const containerRect = container.getBoundingClientRect();
160
+ const range = state.max - state.min;
161
+ const handleWidth = handle.getBoundingClientRect().width || 20;
162
+
163
+ const leftEdge = containerRect.left + (handleWidth / 2);
164
+ const rightEdge = containerRect.right - (handleWidth / 2);
165
+ const effectiveWidth = rightEdge - leftEdge;
166
+
167
+ const adjustedPosition = Math.max(leftEdge, Math.min(rightEdge, position));
168
+ const percentageFromLeft = (adjustedPosition - leftEdge) / effectiveWidth;
169
+
170
+ return state.min + percentageFromLeft * range;
171
+ } catch (error) {
172
+ console.warn('Error calculating value from position:', error);
173
+ return state.min;
174
+ }
175
+ };
176
+
177
+ /**
178
+ * Rounds a value to the nearest step
179
+ * Used for discrete sliders
180
+ */
181
+ const roundToStep = (value) => {
182
+ const step = state.step;
183
+ if (!step || step <= 0) return value;
184
+
185
+ const steps = Math.round((value - state.min) / step);
186
+ return state.min + (steps * step);
187
+ };
188
+
189
+ /**
190
+ * Clamps a value between min and max
191
+ * Ensures values stay within valid range
192
+ */
193
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
194
+
195
+ /**
196
+ * Updates handle and bubble positions
197
+ * Positions elements according to current state values
198
+ */
199
+ const updateHandlePositions = () => {
200
+ const components = getComponents();
201
+ const { handle, container, valueBubble, secondHandle, secondValueBubble } = components;
202
+
203
+ if (!handle || !container) return;
204
+
205
+ const dims = getTrackDimensions();
206
+ if (!dims) return;
207
+
208
+ const { edgeConstraint } = dims;
209
+
210
+ // Update main handle position
211
+ const percent = getPercentage(state.value);
212
+ const adjustedPercent = mapValueToVisualPercent(percent, edgeConstraint);
213
+
214
+ handle.style.left = `${adjustedPercent}%`;
215
+ handle.style.transform = 'translate(-50%, -50%)';
216
+
217
+ if (valueBubble) {
218
+ valueBubble.style.left = `${adjustedPercent}%`;
219
+ valueBubble.style.transform = 'translateX(-50%)';
220
+ }
221
+
222
+ // Update second handle if range slider
223
+ if (config.range && secondHandle && state.secondValue !== null) {
224
+ const secondPercent = getPercentage(state.secondValue);
225
+ const adjustedSecondPercent = mapValueToVisualPercent(secondPercent, edgeConstraint);
226
+
227
+ secondHandle.style.left = `${adjustedSecondPercent}%`;
228
+ secondHandle.style.transform = 'translate(-50%, -50%)';
229
+
230
+ if (secondValueBubble) {
231
+ secondValueBubble.style.left = `${adjustedSecondPercent}%`;
232
+ secondValueBubble.style.transform = 'translateX(-50%)';
233
+ }
234
+ }
235
+
236
+ // Update ARIA attributes
237
+ handle.setAttribute('aria-valuenow', String(state.value));
238
+ if (config.range && secondHandle && state.secondValue !== null) {
239
+ secondHandle.setAttribute('aria-valuenow', String(state.secondValue));
240
+ }
241
+ };
242
+
243
+ /**
244
+ * Updates all track segments
245
+ * Handles different rendering for single and range sliders
246
+ */
247
+ const updateTrackSegments = () => {
248
+ const components = getComponents();
249
+ const { track, container, handle, startTrack, activeTrack, remainingTrack } = components;
250
+
251
+ if (!track || !container || !handle) return;
252
+
253
+ // Safety check for required elements
254
+ if (!activeTrack || !remainingTrack) {
255
+ console.warn('Missing track segments, cannot update track display');
256
+ return;
257
+ }
258
+
259
+ const dims = getTrackDimensions();
260
+ if (!dims) return;
261
+
262
+ const { handleSize, trackSize, paddingPercent } = dims;
263
+ const edgeConstraint = (handleSize / 2) / trackSize * 100;
264
+
265
+ if (config.range && state.secondValue !== null) {
266
+ // Range slider setup
267
+ const lowerValue = Math.min(state.value, state.secondValue);
268
+ const higherValue = Math.max(state.value, state.secondValue);
269
+ const lowerPercent = getPercentage(lowerValue);
270
+ const higherPercent = getPercentage(higherValue);
271
+
272
+ const adjustedLower = mapValueToVisualPercent(lowerPercent, edgeConstraint);
273
+ const adjustedHigher = mapValueToVisualPercent(higherPercent, edgeConstraint);
274
+
275
+ // Start track (before first handle)
276
+ if (startTrack) {
277
+ if (lowerPercent > 1) {
278
+ startTrack.style.display = 'block';
279
+ startTrack.style.left = '0';
280
+ startTrack.style.right = `${100 - (adjustedLower - paddingPercent)}%`;
281
+ startTrack.style.width = 'auto';
282
+ } else {
283
+ startTrack.style.display = 'none';
284
+ }
285
+ }
286
+
287
+ // Active track (between handles)
288
+ const valueDiffPercent = Math.abs(higherPercent - lowerPercent);
289
+ const hideThreshold = (handleSize / trackSize) * 100;
290
+
291
+ if (valueDiffPercent <= hideThreshold) {
292
+ activeTrack.style.display = 'none';
293
+ } else {
294
+ activeTrack.style.display = 'block';
295
+ activeTrack.style.left = `${adjustedLower + paddingPercent}%`;
296
+ activeTrack.style.right = `${100 - (adjustedHigher - paddingPercent)}%`;
297
+ activeTrack.style.width = 'auto';
298
+ }
299
+
300
+ // Remaining track (after second handle)
301
+ remainingTrack.style.display = 'block';
302
+ remainingTrack.style.left = `${adjustedHigher + paddingPercent}%`;
303
+ remainingTrack.style.right = '0';
304
+ remainingTrack.style.width = 'auto';
305
+ } else {
306
+ // Single handle slider
307
+ const valuePercent = getPercentage(state.value);
308
+ const adjustedPercent = mapValueToVisualPercent(valuePercent, edgeConstraint);
309
+
310
+ // Hide start track for single slider
311
+ if (startTrack) {
312
+ startTrack.style.display = 'none';
313
+ }
314
+
315
+ // Active track (filled part)
316
+ activeTrack.style.display = 'block';
317
+ activeTrack.style.left = '0';
318
+ activeTrack.style.right = `${100 - (adjustedPercent - paddingPercent)}%`;
319
+ activeTrack.style.width = 'auto';
320
+
321
+ // Remaining track (unfilled part)
322
+ remainingTrack.style.display = 'block';
323
+ remainingTrack.style.left = `${adjustedPercent + paddingPercent}%`;
324
+ remainingTrack.style.right = '0';
325
+ remainingTrack.style.width = 'auto';
326
+ }
327
+ };
328
+
329
+ /**
330
+ * Updates value bubble content
331
+ * Applies formatting to displayed values
332
+ */
333
+ const updateValueBubbles = () => {
334
+ const components = getComponents();
335
+ const { valueBubble, secondValueBubble } = components;
336
+
337
+ if (!valueBubble) return;
338
+
339
+ const formatter = config.valueFormatter || (value => value.toString());
340
+ valueBubble.textContent = formatter(state.value);
341
+
342
+ if (config.range && secondValueBubble && state.secondValue !== null) {
343
+ secondValueBubble.textContent = formatter(state.secondValue);
344
+ }
345
+ };
346
+
347
+ /**
348
+ * Shows or hides value bubble
349
+ * Controls visibility for value indicators
350
+ */
351
+ const showValueBubble = (bubbleElement, show) => {
352
+ if (!bubbleElement || !config.showValue) return;
353
+
354
+ const bubbleClass = state.component.getClass('slider-value');
355
+ bubbleElement.classList[show ? 'add' : 'remove'](`${bubbleClass}--visible`);
356
+ };
357
+
358
+ /**
359
+ * Generates tick marks
360
+ * Creates visual indicators for discrete values
361
+ */
362
+ const generateTicks = () => {
363
+ const components = getComponents();
364
+ const { ticksContainer, container } = components;
365
+
366
+ if (!ticksContainer || !container) return;
367
+
368
+ // Clear existing ticks
369
+ while (ticksContainer.firstChild) {
370
+ ticksContainer.removeChild(ticksContainer.firstChild);
371
+ }
372
+
373
+ state.ticks = [];
374
+ if (!config.ticks) return;
375
+
376
+ const numSteps = Math.floor((state.max - state.min) / state.step);
377
+ const tickValues = [];
378
+
379
+ // Generate tick values
380
+ for (let i = 0; i <= numSteps; i++) {
381
+ const value = state.min + (i * state.step);
382
+ if (value <= state.max) tickValues.push(value);
383
+ }
384
+
385
+ // Ensure max value is included
386
+ if (tickValues[tickValues.length - 1] !== state.max) {
387
+ tickValues.push(state.max);
388
+ }
389
+
390
+ // CSS classes
391
+ const tickClass = state.component.getClass('slider-tick');
392
+ const activeClass = `${tickClass}--active`;
393
+ const inactiveClass = `${tickClass}--inactive`;
394
+ const hiddenClass = `${tickClass}--hidden`;
395
+
396
+ // Create tick elements
397
+ tickValues.forEach(value => {
398
+ const percent = getPercentage(value);
399
+ const tick = document.createElement('div');
400
+ tick.classList.add(tickClass);
401
+ tick.style.left = `${percent}%`;
402
+
403
+ // Determine tick active state
404
+ const isExactlySelected = (value === state.value ||
405
+ (config.range && state.secondValue !== null && value === state.secondValue));
406
+
407
+ if (isExactlySelected) {
408
+ tick.classList.add(hiddenClass);
409
+ } else if (config.range && state.secondValue !== null) {
410
+ const lowerValue = Math.min(state.value, state.secondValue);
411
+ const higherValue = Math.max(state.value, state.secondValue);
412
+
413
+ tick.classList.add(value >= lowerValue && value <= higherValue ? activeClass : inactiveClass);
414
+ } else {
415
+ tick.classList.add(value <= state.value ? activeClass : inactiveClass);
416
+ }
417
+
418
+ ticksContainer.appendChild(tick);
419
+ state.ticks.push(tick);
420
+ });
421
+ };
422
+
423
+ /**
424
+ * Updates active state of tick marks
425
+ * Sets visual state based on current values
426
+ */
427
+ const updateTicks = () => {
428
+ if (!state.ticks || state.ticks.length === 0) return;
429
+
430
+ const tickClass = state.component.getClass('slider-tick');
431
+ const activeClass = `${tickClass}--active`;
432
+ const inactiveClass = `${tickClass}--inactive`;
433
+ const hiddenClass = `${tickClass}--hidden`;
434
+
435
+ state.ticks.forEach((tick, index) => {
436
+ // Safety check for null tick
437
+ if (!tick) return;
438
+
439
+ // Calculate the value for this tick
440
+ const tickValue = state.min + (index * state.step);
441
+
442
+ // Reset visibility first
443
+ tick.classList.remove(hiddenClass);
444
+
445
+ // Check if this tick should be hidden (matches exactly a selected value)
446
+ const isExactlySelected = (tickValue === state.value ||
447
+ (config.range && state.secondValue !== null && tickValue === state.secondValue));
448
+
449
+ if (isExactlySelected) {
450
+ tick.classList.add(hiddenClass);
451
+ } else if (config.range && state.secondValue !== null) {
452
+ // Range slider - ticks between values should be active
453
+ const lowerValue = Math.min(state.value, state.secondValue);
454
+ const higherValue = Math.max(state.value, state.secondValue);
455
+
456
+ if (tickValue >= lowerValue && tickValue <= higherValue) {
457
+ tick.classList.add(activeClass);
458
+ tick.classList.remove(inactiveClass);
459
+ } else {
460
+ tick.classList.remove(activeClass);
461
+ tick.classList.add(inactiveClass);
462
+ }
463
+ } else {
464
+ // Single slider - ticks below value should be active
465
+ if (tickValue <= state.value) {
466
+ tick.classList.add(activeClass);
467
+ tick.classList.remove(inactiveClass);
468
+ } else {
469
+ tick.classList.remove(activeClass);
470
+ tick.classList.add(inactiveClass);
471
+ }
472
+ }
473
+ });
474
+ };
475
+
476
+ /**
477
+ * Renders all UI elements to match current state
478
+ * Central method for keeping UI in sync with state
479
+ */
480
+ const render = () => {
481
+ try {
482
+ updateHandlePositions();
483
+ updateTrackSegments();
484
+ updateValueBubbles();
485
+ updateTicks();
486
+ } catch (error) {
487
+ console.warn('Error rendering UI:', error);
488
+ }
489
+ };
490
+
491
+ // Create UI renderer interface for event handlers
492
+ const uiRenderer = {
493
+ getPercentage,
494
+ getValueFromPosition,
495
+ roundToStep,
496
+ clamp,
497
+ showValueBubble,
498
+ generateTicks,
499
+ updateTicks,
500
+ updateUi: render, // For backward compatibility
501
+ render
502
+ };
503
+
504
+ // Create event handlers with our renderer
505
+ const handlers = createHandlers(config, state, uiRenderer, eventHelpers);
506
+
507
+ // Initialize slider controller
508
+ const initController = () => {
509
+ try {
510
+ // Verify we have the necessary components
511
+ if (!component.components || !component.components.handle) {
512
+ console.warn('Cannot initialize slider controller: missing required components');
513
+ return;
514
+ }
515
+
516
+ // Set ARIA attributes
517
+ component.element.setAttribute('aria-valuemin', String(state.min));
518
+ component.element.setAttribute('aria-valuemax', String(state.max));
519
+ component.element.setAttribute('aria-valuenow', String(state.value));
520
+
521
+ const { handle, secondHandle } = component.components;
522
+
523
+ // Set handle attributes
524
+ if (handle) {
525
+ handle.setAttribute('aria-valuemin', String(state.min));
526
+ handle.setAttribute('aria-valuemax', String(state.max));
527
+ handle.setAttribute('aria-valuenow', String(state.value));
528
+
529
+ if (config.range && secondHandle && state.secondValue !== null) {
530
+ secondHandle.setAttribute('aria-valuemin', String(state.min));
531
+ secondHandle.setAttribute('aria-valuemax', String(state.max));
532
+ secondHandle.setAttribute('aria-valuenow', String(state.secondValue));
533
+ }
534
+ }
535
+
536
+ // Setup event listeners
537
+ handlers.setupEventListeners();
538
+
539
+ // Initially generate ticks if needed
540
+ if (config.ticks || config.tickLabels) {
541
+ generateTicks();
542
+ }
543
+
544
+ // Initial UI update
545
+ render();
546
+
547
+ // Force one more UI update after a delay to ensure proper positioning
548
+ setTimeout(() => {
549
+ render();
550
+ }, 50);
551
+ } catch (error) {
552
+ console.error('Error initializing slider controller:', error);
553
+ }
554
+ };
555
+
556
+ // Register with lifecycle if available
557
+ if (component.lifecycle) {
558
+ const originalDestroy = component.lifecycle.destroy || (() => {});
559
+ component.lifecycle.destroy = () => {
560
+ handlers.cleanupEventListeners();
561
+ originalDestroy();
562
+ };
563
+ }
564
+
565
+ // Schedule initialization after current execution completes
566
+ setTimeout(() => {
567
+ initController();
568
+ }, 0);
569
+
570
+ // Return enhanced component
571
+ return {
572
+ ...component,
573
+ // Provide controller API under 'slider' property for backward compatibility
574
+ slider: {
575
+ /**
576
+ * Sets slider value
577
+ * @param value New value
578
+ * @param triggerEvent Whether to trigger change event
579
+ * @returns Slider controller for chaining
580
+ */
581
+ setValue(value, triggerEvent = true) {
582
+ const newValue = clamp(value, state.min, state.max);
583
+ state.value = newValue;
584
+ render();
585
+
586
+ if (triggerEvent) {
587
+ eventHelpers.triggerEvent(SLIDER_EVENTS.CHANGE);
588
+ }
589
+
590
+ return this;
591
+ },
592
+
593
+ /**
594
+ * Gets slider value
595
+ * @returns Current value
596
+ */
597
+ getValue() {
598
+ return state.value;
599
+ },
600
+
601
+ /**
602
+ * Sets secondary slider value (for range slider)
603
+ * @param value New secondary value
604
+ * @param triggerEvent Whether to trigger change event
605
+ * @returns Slider controller for chaining
606
+ */
607
+ setSecondValue(value, triggerEvent = true) {
608
+ if (!config.range) return this;
609
+
610
+ const newValue = clamp(value, state.min, state.max);
611
+ state.secondValue = newValue;
612
+ render();
613
+
614
+ if (triggerEvent) {
615
+ eventHelpers.triggerEvent(SLIDER_EVENTS.CHANGE);
616
+ }
617
+
618
+ return this;
619
+ },
620
+
621
+ /**
622
+ * Gets secondary slider value
623
+ * @returns Current secondary value or null
624
+ */
625
+ getSecondValue() {
626
+ return config.range ? state.secondValue : null;
627
+ },
628
+
629
+ /**
630
+ * Sets slider minimum value
631
+ * @param min New minimum value
632
+ * @returns Slider controller for chaining
633
+ */
634
+ setMin(min) {
635
+ state.min = min;
636
+
637
+ // Update ARIA attributes if elements exist
638
+ component.element.setAttribute('aria-valuemin', String(min));
639
+ if (component.components?.handle) {
640
+ component.components.handle.setAttribute('aria-valuemin', String(min));
641
+ }
642
+
643
+ if (config.range && component.components?.secondHandle) {
644
+ component.components.secondHandle.setAttribute('aria-valuemin', String(min));
645
+ }
646
+
647
+ // Clamp values to new min
648
+ if (state.value < min) state.value = min;
649
+ if (config.range && state.secondValue !== null && state.secondValue < min) {
650
+ state.secondValue = min;
651
+ }
652
+
653
+ // Regenerate ticks if needed
654
+ if (config.ticks || config.tickLabels) {
655
+ generateTicks();
656
+ }
657
+
658
+ render();
659
+ return this;
660
+ },
661
+
662
+ /**
663
+ * Gets slider minimum value
664
+ * @returns Current minimum value
665
+ */
666
+ getMin() {
667
+ return state.min;
668
+ },
669
+
670
+ /**
671
+ * Sets slider maximum value
672
+ * @param max New maximum value
673
+ * @returns Slider controller for chaining
674
+ */
675
+ setMax(max) {
676
+ state.max = max;
677
+
678
+ // Update ARIA attributes if elements exist
679
+ component.element.setAttribute('aria-valuemax', String(max));
680
+ if (component.components?.handle) {
681
+ component.components.handle.setAttribute('aria-valuemax', String(max));
682
+ }
683
+
684
+ if (config.range && component.components?.secondHandle) {
685
+ component.components.secondHandle.setAttribute('aria-valuemax', String(max));
686
+ }
687
+
688
+ // Clamp values to new max
689
+ if (state.value > max) state.value = max;
690
+ if (config.range && state.secondValue !== null && state.secondValue > max) {
691
+ state.secondValue = max;
692
+ }
693
+
694
+ // Regenerate ticks if needed
695
+ if (config.ticks || config.tickLabels) {
696
+ generateTicks();
697
+ }
698
+
699
+ render();
700
+ return this;
701
+ },
702
+
703
+ /**
704
+ * Gets slider maximum value
705
+ * @returns Current maximum value
706
+ */
707
+ getMax() {
708
+ return state.max;
709
+ },
710
+
711
+ /**
712
+ * Sets slider step size
713
+ * @param step New step size
714
+ * @returns Slider controller for chaining
715
+ */
716
+ setStep(step) {
717
+ state.step = step;
718
+
719
+ // Add or remove discrete class
720
+ component.element.classList[step > 0 ? 'add' : 'remove'](
721
+ `${component.getClass('slider')}--discrete`
722
+ );
723
+
724
+ // Regenerate ticks if needed
725
+ if (config.ticks || config.tickLabels) {
726
+ generateTicks();
727
+ updateTicks();
728
+ }
729
+
730
+ return this;
731
+ },
732
+
733
+ /**
734
+ * Gets slider step size
735
+ * @returns Current step size
736
+ */
737
+ getStep() {
738
+ return state.step;
739
+ },
740
+
741
+ /**
742
+ * Regenerate tick marks and labels
743
+ * @returns Slider controller for chaining
744
+ */
745
+ regenerateTicks() {
746
+ generateTicks();
747
+ updateTicks();
748
+ return this;
749
+ },
750
+
751
+ /**
752
+ * Update all UI elements
753
+ * @returns Slider controller for chaining
754
+ */
755
+ updateUi() {
756
+ render();
757
+ return this;
758
+ }
759
+ }
760
+ };
761
+ };