kempo-ui 0.4.1 → 0.4.2

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 (74) hide show
  1. package/dist/components/Slider.js +188 -0
  2. package/docs/components/accordion.html +4 -0
  3. package/docs/components/aside.html +4 -0
  4. package/docs/components/card.html +4 -0
  5. package/docs/components/code-editor.html +4 -0
  6. package/docs/components/color-picker.html +4 -0
  7. package/docs/components/combobox.html +4 -0
  8. package/docs/components/content-slider.html +4 -0
  9. package/docs/components/context.html +4 -0
  10. package/docs/components/dialog.html +4 -0
  11. package/docs/components/dropdown.html +4 -0
  12. package/docs/components/filter-list.html +4 -0
  13. package/docs/components/focus-capture.html +4 -0
  14. package/docs/components/html-editor.html +4 -0
  15. package/docs/components/hybrid-component.html +4 -0
  16. package/docs/components/icon.html +4 -0
  17. package/docs/components/import.html +4 -0
  18. package/docs/components/light-component.html +4 -0
  19. package/docs/components/nav-spacer.html +4 -0
  20. package/docs/components/nav.html +4 -0
  21. package/docs/components/photo-viewer.html +4 -0
  22. package/docs/components/progress.html +4 -0
  23. package/docs/components/resize.html +4 -0
  24. package/docs/components/shadow-component.html +4 -0
  25. package/docs/components/show-more.html +4 -0
  26. package/docs/components/slider.html +781 -0
  27. package/docs/components/sortable.html +4 -0
  28. package/docs/components/spinner.html +4 -0
  29. package/docs/components/split.html +4 -0
  30. package/docs/components/table.html +4 -0
  31. package/docs/components/tableControls.html +4 -0
  32. package/docs/components/tableCustomFields.html +4 -0
  33. package/docs/components/tableFetchRecords.html +4 -0
  34. package/docs/components/tableFieldSortHide.html +4 -0
  35. package/docs/components/tablePagination.html +4 -0
  36. package/docs/components/tablePlaceholder.html +4 -0
  37. package/docs/components/tableRecordEditing.html +4 -0
  38. package/docs/components/tableRecordFiltering.html +4 -0
  39. package/docs/components/tableRecordHiding.html +4 -0
  40. package/docs/components/tableRecordSearching.html +4 -0
  41. package/docs/components/tableRecordSelection.html +4 -0
  42. package/docs/components/tableRowControls.html +4 -0
  43. package/docs/components/tableServerSync.html +4 -0
  44. package/docs/components/tableSorting.html +4 -0
  45. package/docs/components/tabs.html +4 -0
  46. package/docs/components/tags.html +4 -0
  47. package/docs/components/theme-select.html +4 -0
  48. package/docs/components/theme-switcher.html +4 -0
  49. package/docs/components/timestamp.html +4 -0
  50. package/docs/components/toast.html +4 -0
  51. package/docs/components/toggle.html +4 -0
  52. package/docs/components/tree.html +4 -0
  53. package/docs/index.html +10 -0
  54. package/docs/src/components/Slider.js +188 -0
  55. package/docs/utils/context.html +4 -0
  56. package/docs/utils/cookie.html +4 -0
  57. package/docs/utils/debounce.html +4 -0
  58. package/docs/utils/drag.html +4 -0
  59. package/docs/utils/elevation.html +4 -0
  60. package/docs/utils/formatTimestamp.html +4 -0
  61. package/docs/utils/object.html +4 -0
  62. package/docs/utils/propConverters.html +4 -0
  63. package/docs/utils/string.html +4 -0
  64. package/docs/utils/theme.html +4 -0
  65. package/docs/utils/toTitleCase.html +4 -0
  66. package/docs/utils/type.html +4 -0
  67. package/docs/utils/wait.html +4 -0
  68. package/docs-src/components/slider.page.html +331 -0
  69. package/docs-src/index.page.html +6 -0
  70. package/docs-src/nav.fragment.html +4 -0
  71. package/llms.txt +1 -0
  72. package/package.json +1 -1
  73. package/src/components/Slider.js +480 -0
  74. package/tests/components/Slider.browser-test.js +823 -0
@@ -0,0 +1,480 @@
1
+ import { html, css } from '../lit-all.min.js';
2
+ import ShadowComponent from './ShadowComponent.js';
3
+
4
+ export default class Slider extends ShadowComponent {
5
+ static formAssociated = true;
6
+
7
+ static properties = {
8
+ value: { type: String, reflect: true },
9
+ name: { type: String, reflect: true },
10
+ min: { type: Number, reflect: true },
11
+ max: { type: Number, reflect: true },
12
+ steps: { type: String, reflect: true },
13
+ format: { type: String, reflect: true },
14
+ tooltip: { type: Boolean, reflect: true },
15
+ vertical: { type: Boolean, reflect: true },
16
+ disabled: { type: Boolean, reflect: true }
17
+ };
18
+
19
+ #activeThumb = null;
20
+
21
+ /*
22
+ Lifecycle Callbacks
23
+ */
24
+ constructor() {
25
+ super();
26
+ this.internals = this.attachInternals();
27
+ this.value = '0';
28
+ this.name = '';
29
+ this.min = 0;
30
+ this.max = 100;
31
+ this.steps = null;
32
+ this.format = null;
33
+ this.tooltip = false;
34
+ this.vertical = false;
35
+ this.disabled = false;
36
+ this.tabIndex = 0;
37
+ }
38
+
39
+ connectedCallback() {
40
+ super.connectedCallback();
41
+ this.addEventListener('keydown', this.handleKeyDown);
42
+ }
43
+
44
+ disconnectedCallback() {
45
+ super.disconnectedCallback();
46
+ this.removeEventListener('keydown', this.handleKeyDown);
47
+ }
48
+
49
+ get isRange() {
50
+ return String(this.value).includes(',');
51
+ }
52
+
53
+ get formattedValue() {
54
+ if(!this.format) return this.value;
55
+ if(this.isRange) return `${this.formatValue(this.lower)},${this.formatValue(this.upper)}`;
56
+ return this.formatValue(this.lower);
57
+ }
58
+
59
+ updated(changedProperties) {
60
+ super.updated(changedProperties);
61
+ this.internals.setFormValue(this.formattedValue);
62
+ if(changedProperties.has('value') && changedProperties.get('value') !== undefined) {
63
+ this.dispatchEvent(new CustomEvent('change', {
64
+ detail: { value: this.formattedValue },
65
+ bubbles: true
66
+ }));
67
+ }
68
+ }
69
+
70
+ /*
71
+ Utility
72
+ */
73
+ get stepValues() {
74
+ if(!this.steps) return null;
75
+ return this.steps.split(',').map(s => Number(s.trim())).filter(n => !isNaN(n)).sort((a, b) => a - b);
76
+ }
77
+
78
+ get lower() {
79
+ const parts = String(this.value).split(',');
80
+ return Number(parts[0]) || 0;
81
+ }
82
+
83
+ get upper() {
84
+ const parts = String(this.value).split(',');
85
+ return parts.length > 1 ? Number(parts[1]) : this.max;
86
+ }
87
+
88
+ get percentage() {
89
+ return ((this.lower - this.min) / (this.max - this.min)) * 100;
90
+ }
91
+
92
+ get upperPercentage() {
93
+ return ((this.upper - this.min) / (this.max - this.min)) * 100;
94
+ }
95
+
96
+ formatValue = (num) => {
97
+ if(!this.format) return String(num);
98
+ const pattern = this.format;
99
+ const match = pattern.match(/0([^0]?)0+/);
100
+ if(!match) return pattern.replace('0', String(Math.round(num)));
101
+ const sep = match[0].indexOf(match[1]) > 0 ? match[1] : '';
102
+ const decimals = sep ? match[0].split(sep)[1].length : 0;
103
+ const fixed = num.toFixed(decimals);
104
+ return pattern.replace(match[0], fixed);
105
+ };
106
+
107
+ snapToNearest = (val) => {
108
+ const sv = this.stepValues;
109
+ if(!sv || sv.length === 0) return Math.min(this.max, Math.max(this.min, val));
110
+ let closest = sv[0];
111
+ let closestDist = Math.abs(val - closest);
112
+ for(let i = 1; i < sv.length; i++) {
113
+ const dist = Math.abs(val - sv[i]);
114
+ if(dist < closestDist) {
115
+ closest = sv[i];
116
+ closestDist = dist;
117
+ }
118
+ }
119
+ return closest;
120
+ };
121
+
122
+ ratioFromEvent = (e) => {
123
+ const track = this.shadowRoot.querySelector('#track');
124
+ const rect = track.getBoundingClientRect();
125
+ if(this.vertical) {
126
+ return Math.min(1, Math.max(0, 1 - (e.clientY - rect.top) / rect.height));
127
+ }
128
+ return Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
129
+ };
130
+
131
+ valueFromEvent = (e) => {
132
+ const raw = this.min + this.ratioFromEvent(e) * (this.max - this.min);
133
+ return this.snapToNearest(raw);
134
+ };
135
+
136
+ setValue = (newValue) => {
137
+ if(this.disabled) return;
138
+ const clamped = Math.min(this.max, Math.max(this.min, newValue));
139
+ const snapped = this.snapToNearest(clamped);
140
+ if(this.isRange) {
141
+ if(snapped > this.upper) return;
142
+ if(snapped !== this.lower) {
143
+ this.value = `${snapped},${this.upper}`;
144
+ }
145
+ } else {
146
+ if(snapped !== this.lower) {
147
+ this.value = String(snapped);
148
+ }
149
+ }
150
+ };
151
+
152
+ setUpper = (newValue) => {
153
+ if(this.disabled || !this.isRange) return;
154
+ const clamped = Math.min(this.max, Math.max(this.min, newValue));
155
+ const snapped = this.snapToNearest(clamped);
156
+ if(snapped < this.lower) return;
157
+ if(snapped !== this.upper) {
158
+ this.value = `${this.lower},${snapped}`;
159
+ }
160
+ };
161
+
162
+ closestThumb = (val) => {
163
+ if(!this.isRange) return 'lower';
164
+ const distLower = Math.abs(val - this.lower);
165
+ const distUpper = Math.abs(val - this.upper);
166
+ return distLower <= distUpper ? 'lower' : 'upper';
167
+ };
168
+
169
+ stepIncrement = (current, direction) => {
170
+ const sv = this.stepValues;
171
+ if(sv && sv.length > 0) {
172
+ const idx = sv.indexOf(current);
173
+ if(direction > 0) {
174
+ if(idx >= 0 && idx < sv.length - 1) return sv[idx + 1];
175
+ if(idx === -1) return sv.find(v => v > current) ?? sv[sv.length - 1];
176
+ } else {
177
+ if(idx > 0) return sv[idx - 1];
178
+ if(idx === -1) return [...sv].reverse().find(v => v < current) ?? sv[0];
179
+ }
180
+ return current;
181
+ }
182
+ return current + direction;
183
+ };
184
+
185
+ /*
186
+ Event Handlers
187
+ */
188
+ handleTrackClick = (e) => {
189
+ if(this.disabled) return;
190
+ const val = this.valueFromEvent(e);
191
+ if(this.isRange) {
192
+ if(this.closestThumb(val) === 'upper') this.setUpper(val);
193
+ else this.setValue(val);
194
+ } else {
195
+ this.setValue(val);
196
+ }
197
+ };
198
+
199
+ handleThumbDown = (which, e) => {
200
+ if(this.disabled) return;
201
+ e.preventDefault();
202
+ e.stopPropagation();
203
+ this.#activeThumb = which;
204
+ this.requestUpdate();
205
+ const onMove = (ev) => {
206
+ const clientEv = ev.touches ? ev.touches[0] : ev;
207
+ const val = this.valueFromEvent(clientEv);
208
+ if(this.#activeThumb === 'upper') this.setUpper(val);
209
+ else this.setValue(val);
210
+ };
211
+ const onUp = () => {
212
+ this.#activeThumb = null;
213
+ this.requestUpdate();
214
+ window.removeEventListener('mousemove', onMove);
215
+ window.removeEventListener('mouseup', onUp);
216
+ window.removeEventListener('touchmove', onMove);
217
+ window.removeEventListener('touchend', onUp);
218
+ };
219
+ window.addEventListener('mousemove', onMove);
220
+ window.addEventListener('mouseup', onUp);
221
+ window.addEventListener('touchmove', onMove);
222
+ window.addEventListener('touchend', onUp);
223
+ };
224
+
225
+ handleKeyDown = (e) => {
226
+ if(this.disabled) return;
227
+ const sv = this.stepValues;
228
+ const isIncrease = e.key === 'ArrowRight' || e.key === 'ArrowUp';
229
+ const isDecrease = e.key === 'ArrowLeft' || e.key === 'ArrowDown';
230
+ if(isIncrease || isDecrease) {
231
+ e.preventDefault();
232
+ const dir = isIncrease ? 1 : -1;
233
+ if(this.isRange && this.#activeThumb === 'upper') {
234
+ this.setUpper(this.stepIncrement(this.upper, dir));
235
+ } else {
236
+ this.setValue(this.stepIncrement(this.lower, dir));
237
+ }
238
+ } else if(e.key === 'Home') {
239
+ e.preventDefault();
240
+ this.setValue(sv ? sv[0] : this.min);
241
+ } else if(e.key === 'End') {
242
+ e.preventDefault();
243
+ if(this.isRange) this.setUpper(sv ? sv[sv.length - 1] : this.max);
244
+ else this.setValue(sv ? sv[sv.length - 1] : this.max);
245
+ }
246
+ };
247
+
248
+ /*
249
+ Rendering
250
+ */
251
+ renderHorizontal = () => {
252
+ const pct = this.percentage;
253
+ const sv = this.stepValues;
254
+ if(this.isRange) {
255
+ const upperPct = this.upperPercentage;
256
+ return html`
257
+ <div id="track" @click=${this.handleTrackClick}>
258
+ <div id="fill" style="left:${pct}%;width:${upperPct - pct}%"></div>
259
+ ${sv ? sv.map(step => {
260
+ const stepPct = ((step - this.min) / (this.max - this.min)) * 100;
261
+ return html`<div class="step-dot" style="left:${stepPct}%"></div>`;
262
+ }) : ''}
263
+ <div
264
+ class="thumb${this.#activeThumb === 'lower' ? ' active' : ''}"
265
+ style="left:${pct}%"
266
+ @mousedown=${(e) => this.handleThumbDown('lower', e)}
267
+ @touchstart=${(e) => this.handleThumbDown('lower', e)}
268
+ >${this.tooltip && this.#activeThumb === 'lower' ? html`<div class="tooltip">${this.formatValue(this.lower)}</div>` : ''}</div>
269
+ <div
270
+ class="thumb${this.#activeThumb === 'upper' ? ' active' : ''}"
271
+ style="left:${upperPct}%"
272
+ @mousedown=${(e) => this.handleThumbDown('upper', e)}
273
+ @touchstart=${(e) => this.handleThumbDown('upper', e)}
274
+ >${this.tooltip && this.#activeThumb === 'upper' ? html`<div class="tooltip">${this.formatValue(this.upper)}</div>` : ''}</div>
275
+ </div>
276
+ `;
277
+ }
278
+ return html`
279
+ <div id="track" @click=${this.handleTrackClick}>
280
+ <div id="fill" style="width:${pct}%"></div>
281
+ ${sv ? sv.map(step => {
282
+ const stepPct = ((step - this.min) / (this.max - this.min)) * 100;
283
+ return html`<div class="step-dot" style="left:${stepPct}%"></div>`;
284
+ }) : ''}
285
+ <div
286
+ class="thumb${this.#activeThumb === 'lower' ? ' active' : ''}"
287
+ style="left:${pct}%"
288
+ @mousedown=${(e) => this.handleThumbDown('lower', e)}
289
+ @touchstart=${(e) => this.handleThumbDown('lower', e)}
290
+ >${this.tooltip && this.#activeThumb === 'lower' ? html`<div class="tooltip">${this.formatValue(this.lower)}</div>` : ''}</div>
291
+ </div>
292
+ `;
293
+ };
294
+
295
+ renderVertical = () => {
296
+ const pct = this.percentage;
297
+ const sv = this.stepValues;
298
+ if(this.isRange) {
299
+ const upperPct = this.upperPercentage;
300
+ return html`
301
+ <div id="track" @click=${this.handleTrackClick}>
302
+ <div id="fill" style="bottom:${pct}%;height:${upperPct - pct}%"></div>
303
+ ${sv ? sv.map(step => {
304
+ const stepPct = ((step - this.min) / (this.max - this.min)) * 100;
305
+ return html`<div class="step-dot" style="bottom:${stepPct}%"></div>`;
306
+ }) : ''}
307
+ <div
308
+ class="thumb${this.#activeThumb === 'lower' ? ' active' : ''}"
309
+ style="bottom:${pct}%"
310
+ @mousedown=${(e) => this.handleThumbDown('lower', e)}
311
+ @touchstart=${(e) => this.handleThumbDown('lower', e)}
312
+ >${this.tooltip && this.#activeThumb === 'lower' ? html`<div class="tooltip">${this.formatValue(this.lower)}</div>` : ''}</div>
313
+ <div
314
+ class="thumb${this.#activeThumb === 'upper' ? ' active' : ''}"
315
+ style="bottom:${upperPct}%"
316
+ @mousedown=${(e) => this.handleThumbDown('upper', e)}
317
+ @touchstart=${(e) => this.handleThumbDown('upper', e)}
318
+ >${this.tooltip && this.#activeThumb === 'upper' ? html`<div class="tooltip">${this.formatValue(this.upper)}</div>` : ''}</div>
319
+ </div>
320
+ `;
321
+ }
322
+ return html`
323
+ <div id="track" @click=${this.handleTrackClick}>
324
+ <div id="fill" style="height:${pct}%"></div>
325
+ ${sv ? sv.map(step => {
326
+ const stepPct = ((step - this.min) / (this.max - this.min)) * 100;
327
+ return html`<div class="step-dot" style="bottom:${stepPct}%"></div>`;
328
+ }) : ''}
329
+ <div
330
+ class="thumb${this.#activeThumb === 'lower' ? ' active' : ''}"
331
+ style="bottom:${pct}%"
332
+ @mousedown=${(e) => this.handleThumbDown('lower', e)}
333
+ @touchstart=${(e) => this.handleThumbDown('lower', e)}
334
+ >${this.tooltip && this.#activeThumb === 'lower' ? html`<div class="tooltip">${this.formatValue(this.lower)}</div>` : ''}</div>
335
+ </div>
336
+ `;
337
+ };
338
+
339
+ render() {
340
+ return html`
341
+ ${this.vertical ? this.renderVertical() : this.renderHorizontal()}
342
+ <div id="label"><slot></slot></div>
343
+ `;
344
+ }
345
+
346
+ /*
347
+ Styles
348
+ */
349
+ static styles = css`
350
+ :host {
351
+ --track_height: 6px;
352
+ --track_background: var(--c_border);
353
+ --track_radius: 99999px;
354
+ --fill_background: var(--c_primary);
355
+ --thumb_size: 20px;
356
+ --thumb_background: var(--c_primary);
357
+ --thumb_border: 2px solid white;
358
+ --thumb_shadow: var(--focus_shadow);
359
+ --step_dot_size: 8px;
360
+ --step_dot_background: var(--c_bg__alt);
361
+ --step_dot_border: 1px solid var(--c_border);
362
+ --vertical_height: 10rem;
363
+
364
+ display: flex;
365
+ align-items: center;
366
+ gap: 0.5rem;
367
+ cursor: pointer;
368
+ user-select: none;
369
+ }
370
+ :host([disabled]) {
371
+ opacity: 0.5;
372
+ pointer-events: none;
373
+ }
374
+ :host([vertical]) {
375
+ flex-direction: column;
376
+ width: auto;
377
+ display: inline-flex;
378
+ }
379
+ #track {
380
+ flex: 1;
381
+ height: var(--track_height);
382
+ background: var(--track_background);
383
+ border-radius: var(--track_radius);
384
+ position: relative;
385
+ }
386
+ :host([vertical]) #track {
387
+ height: auto;
388
+ width: var(--track_height);
389
+ min-height: var(--vertical_height);
390
+ }
391
+ #fill {
392
+ background: var(--fill_background);
393
+ border-radius: var(--track_radius);
394
+ pointer-events: none;
395
+ }
396
+ :host(:not([vertical])) #fill {
397
+ height: 100%;
398
+ position: absolute;
399
+ top: 0;
400
+ left: 0;
401
+ }
402
+ :host([vertical]) #fill {
403
+ width: 100%;
404
+ position: absolute;
405
+ bottom: 0;
406
+ left: 0;
407
+ }
408
+ .thumb {
409
+ width: var(--thumb_size);
410
+ height: var(--thumb_size);
411
+ background: var(--thumb_background);
412
+ border: var(--thumb_border);
413
+ border-radius: 50%;
414
+ position: absolute;
415
+ box-shadow: 0 0 0 transparent;
416
+ cursor: grab;
417
+ transition: box-shadow 0.15s;
418
+ z-index: 1;
419
+ }
420
+ :host(:not([vertical])) .thumb {
421
+ top: 50%;
422
+ transform: translate(-50%, -50%);
423
+ }
424
+ :host([vertical]) .thumb {
425
+ left: 50%;
426
+ transform: translate(-50%, 50%);
427
+ }
428
+ .thumb:active,
429
+ .thumb.active {
430
+ cursor: grabbing;
431
+ box-shadow: var(--focus_shadow);
432
+ }
433
+ .tooltip {
434
+ position: absolute;
435
+ background: var(--c_text);
436
+ color: var(--c_bg);
437
+ padding: 0.15rem 0.4rem;
438
+ border-radius: 0.25rem;
439
+ font-size: 0.75rem;
440
+ white-space: nowrap;
441
+ pointer-events: none;
442
+ line-height: 1.2;
443
+ }
444
+ :host(:not([vertical])) .tooltip {
445
+ bottom: calc(100% + 6px);
446
+ left: 50%;
447
+ transform: translateX(-50%);
448
+ }
449
+ :host([vertical]) .tooltip {
450
+ right: calc(100% + 6px);
451
+ top: 50%;
452
+ transform: translateY(-50%);
453
+ }
454
+ .step-dot {
455
+ width: var(--step_dot_size);
456
+ height: var(--step_dot_size);
457
+ background: var(--step_dot_background);
458
+ border: var(--step_dot_border);
459
+ border-radius: 50%;
460
+ position: absolute;
461
+ pointer-events: none;
462
+ }
463
+ :host(:not([vertical])) .step-dot {
464
+ top: 50%;
465
+ transform: translate(-50%, -50%);
466
+ }
467
+ :host([vertical]) .step-dot {
468
+ left: 50%;
469
+ transform: translate(-50%, 50%);
470
+ }
471
+ #label {
472
+ font-size: 0.875rem;
473
+ }
474
+ #label:empty {
475
+ display: none;
476
+ }
477
+ `;
478
+ }
479
+
480
+ customElements.define('k-slider', Slider);