@xsynaptic/sonic-ui 0.2.0 → 0.4.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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/default/define/number.js +7 -0
  3. package/dist/default/define.js +1 -0
  4. package/dist/default/elements/dial.js +24 -10
  5. package/dist/default/elements/key.js +29 -4
  6. package/dist/default/elements/meter.js +63 -9
  7. package/dist/default/elements/number.js +46 -0
  8. package/dist/default/elements/range-element.js +172 -36
  9. package/dist/default/elements/slider.js +63 -3
  10. package/dist/default/index.js +2 -1
  11. package/dist/default/lib/detent.js +17 -0
  12. package/dist/default/lib/math.js +16 -0
  13. package/dist/default/lib/number-list.js +14 -0
  14. package/dist/default/lib/taper.js +17 -4
  15. package/dist/dev/define/number.d.ts +2 -0
  16. package/dist/dev/define/number.js +8 -0
  17. package/dist/dev/define/number.js.map +1 -0
  18. package/dist/dev/define.d.ts +3 -1
  19. package/dist/dev/define.js +1 -0
  20. package/dist/dev/elements/dial.d.ts +3 -0
  21. package/dist/dev/elements/dial.js +24 -10
  22. package/dist/dev/elements/dial.js.map +1 -1
  23. package/dist/dev/elements/key.d.ts +4 -0
  24. package/dist/dev/elements/key.js +29 -4
  25. package/dist/dev/elements/key.js.map +1 -1
  26. package/dist/dev/elements/meter.d.ts +6 -0
  27. package/dist/dev/elements/meter.js +63 -9
  28. package/dist/dev/elements/meter.js.map +1 -1
  29. package/dist/dev/elements/number.d.ts +16 -0
  30. package/dist/dev/elements/number.js +47 -0
  31. package/dist/dev/elements/number.js.map +1 -0
  32. package/dist/dev/elements/range-element.d.ts +12 -0
  33. package/dist/dev/elements/range-element.js +172 -36
  34. package/dist/dev/elements/range-element.js.map +1 -1
  35. package/dist/dev/elements/slider.d.ts +8 -0
  36. package/dist/dev/elements/slider.js +63 -3
  37. package/dist/dev/elements/slider.js.map +1 -1
  38. package/dist/dev/index.d.ts +2 -1
  39. package/dist/dev/index.js +2 -1
  40. package/dist/dev/lib/detent.js +18 -0
  41. package/dist/dev/lib/detent.js.map +1 -0
  42. package/dist/dev/lib/math.js +17 -0
  43. package/dist/dev/lib/math.js.map +1 -0
  44. package/dist/dev/lib/number-list.js +15 -0
  45. package/dist/dev/lib/number-list.js.map +1 -0
  46. package/dist/dev/lib/read-px-property.js.map +1 -1
  47. package/dist/dev/lib/taper.js +17 -4
  48. package/dist/dev/lib/taper.js.map +1 -1
  49. package/dist/styles/controls.css +2 -0
  50. package/dist/styles/dial.css +80 -6
  51. package/dist/styles/key.css +96 -11
  52. package/dist/styles/led.css +82 -0
  53. package/dist/styles/material.css +150 -20
  54. package/dist/styles/meter.css +120 -8
  55. package/dist/styles/number.css +81 -0
  56. package/dist/styles/screen.css +6 -39
  57. package/dist/styles/segmented.css +23 -7
  58. package/dist/styles/slider.css +143 -30
  59. package/package.json +4 -2
@@ -2,9 +2,15 @@ import { requireChild } from "../lib/render.js";
2
2
  import { writeAttribute } from "../lib/write-attribute.js";
3
3
  import { SonicFormElement } from "./form-element.js";
4
4
  import { RangeEntry } from "./range-entry.js";
5
- import { clampUnit, linearTaper, logTaper, skewTaper } from "../lib/taper.js";
5
+ import { copyNode } from "../lib/copy-node.js";
6
+ import { passDetent } from "../lib/detent.js";
7
+ import { clamp, clampUnit, trimFloat, wrapUnit } from "../lib/math.js";
8
+ import { nearestEntry, parseNumberList } from "../lib/number-list.js";
9
+ import { readPxProperty } from "../lib/read-px-property.js";
10
+ import { linearTaper, listTaper, logTaper, skewTaper } from "../lib/taper.js";
6
11
 
7
12
  //#region src/elements/range-element.ts
13
+ const detentZonePx = 8;
8
14
  const fineScale = .1;
9
15
  const keyRevealMs = 1e3;
10
16
  const pageSteps = 10;
@@ -13,6 +19,7 @@ const revealPx = 4;
13
19
  const canPopover = "togglePopover" in HTMLElement.prototype;
14
20
  let instanceCount = 0;
15
21
  const resetKeys = /* @__PURE__ */ new Set(["Backspace", "Delete"]);
22
+ const endKeys = /* @__PURE__ */ new Set(["End", "Home"]);
16
23
  const keySteps = /* @__PURE__ */ new Map([
17
24
  ["ArrowDown", -1],
18
25
  ["ArrowLeft", -1],
@@ -21,7 +28,7 @@ const keySteps = /* @__PURE__ */ new Map([
21
28
  ["PageDown", -10],
22
29
  ["PageUp", pageSteps]
23
30
  ]);
24
- const sliderAttributes = [
31
+ const roleAttributes = [
25
32
  "aria-disabled",
26
33
  "aria-orientation",
27
34
  "aria-valuemax",
@@ -31,9 +38,22 @@ const sliderAttributes = [
31
38
  "role",
32
39
  "tabindex"
33
40
  ];
41
+ function scaleValue(mark) {
42
+ const text = mark.dataset.sonicValue?.trim();
43
+ return text ? Number(text) : NaN;
44
+ }
45
+ function scaleMark(original) {
46
+ const mark = copyNode(original);
47
+ if (!(mark instanceof HTMLElement || mark instanceof SVGElement)) return [];
48
+ if (!Number.isFinite(scaleValue(mark))) return [];
49
+ const isTick = mark.childElementCount === 0 && mark.textContent.trim() === "";
50
+ mark.classList.add(isTick ? "sonic-scale-tick" : "sonic-scale-label");
51
+ return [mark];
52
+ }
34
53
  var SonicRangeElement = class extends SonicFormElement {
35
54
  static observedAttributes = [
36
55
  ...SonicFormElement.observedAttributes,
56
+ "detent",
37
57
  "max",
38
58
  "midpoint",
39
59
  "min",
@@ -42,7 +62,8 @@ var SonicRangeElement = class extends SonicFormElement {
42
62
  "readout",
43
63
  "step",
44
64
  "taper",
45
- "value"
65
+ "value",
66
+ "values"
46
67
  ];
47
68
  get default() {
48
69
  return this.optionalNumberAttribute("default");
@@ -50,6 +71,12 @@ var SonicRangeElement = class extends SonicFormElement {
50
71
  set default(value) {
51
72
  this.reflect("default", value);
52
73
  }
74
+ get detent() {
75
+ return this.optionalNumberAttribute("detent");
76
+ }
77
+ set detent(value) {
78
+ this.reflect("detent", value);
79
+ }
53
80
  get dimmed() {
54
81
  return this.hasAttribute("dimmed");
55
82
  }
@@ -130,7 +157,14 @@ var SonicRangeElement = class extends SonicFormElement {
130
157
  if (this.#drag) return;
131
158
  this.#write(next);
132
159
  }
160
+ get values() {
161
+ return parseNumberList(this.getAttribute("values"));
162
+ }
163
+ set values(entries) {
164
+ this.reflect("values", entries?.join(" "));
165
+ }
133
166
  #drag;
167
+ #entries;
134
168
  #entry;
135
169
  #formatValue;
136
170
  #instance = String(instanceCount += 1);
@@ -138,8 +172,10 @@ var SonicRangeElement = class extends SonicFormElement {
138
172
  #keyRevealTimer;
139
173
  #parseValue;
140
174
  #readout;
175
+ #scale;
141
176
  #value = 0;
142
177
  attributeChangedCallback(name) {
178
+ this.#entries = parseNumberList(this.getAttribute("values"));
143
179
  if (name === "value") {
144
180
  if (this.#drag) return;
145
181
  this.#isDirty = false;
@@ -152,7 +188,7 @@ var SonicRangeElement = class extends SonicFormElement {
152
188
  this.render();
153
189
  }
154
190
  connectedCallback() {
155
- this.upgradeProperties("default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "step", "taper", "formatValue", "parseValue", "value");
191
+ this.upgradeProperties("default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "step", "taper", "values", "detent", "formatValue", "parseValue", "value");
156
192
  super.connectedCallback();
157
193
  }
158
194
  bindGestures(control, signal, grab) {
@@ -170,6 +206,7 @@ var SonicRangeElement = class extends SonicFormElement {
170
206
  if (!axis) return;
171
207
  const startPosition = axis.position(event);
172
208
  const drag = {
209
+ detent: this.#detentHold(control, axis.travelPx),
173
210
  fromValue,
174
211
  isRevealed: false,
175
212
  lastPosition: startPosition,
@@ -192,10 +229,10 @@ var SonicRangeElement = class extends SonicFormElement {
192
229
  if (event.pointerId !== drag?.pointerId) return;
193
230
  const position = drag.position(event);
194
231
  const scale = event.shiftKey ? fineScale : 1;
195
- drag.rawFraction = clampUnit(drag.rawFraction + (position - drag.lastPosition) / drag.travelPx * scale);
232
+ const next = this.#move(drag, (position - drag.lastPosition) / drag.travelPx * scale);
196
233
  drag.lastPosition = position;
197
234
  if (Math.abs(position - drag.startPosition) >= revealPx) this.#reveal(drag);
198
- this.input(this.valueAt(drag.rawFraction));
235
+ this.input(next);
199
236
  }, { signal });
200
237
  const endDrag = (event) => {
201
238
  if (event.pointerId === this.#drag?.pointerId) this.#endDrag();
@@ -208,7 +245,7 @@ var SonicRangeElement = class extends SonicFormElement {
208
245
  control.addEventListener("pointerup", (event) => {
209
246
  if (event.pointerId !== this.#drag?.pointerId) return;
210
247
  this.#endDrag();
211
- if (!entry.press(event)) return;
248
+ if (!entry.press(event) || this.springTarget() !== void 0) return;
212
249
  if (this.doublePress === "reset") this.#reset();
213
250
  else entry.open();
214
251
  }, { signal });
@@ -218,8 +255,27 @@ var SonicRangeElement = class extends SonicFormElement {
218
255
  this.#hideForKeys();
219
256
  }, { once: true });
220
257
  }
258
+ bindScale(control, scale, signal) {
259
+ this.#scale = scale;
260
+ this.mirrorChildren({
261
+ control,
262
+ copy: (originals) => {
263
+ scale.replaceChildren(...originals.flatMap((original) => scaleMark(original)));
264
+ this.renderScale();
265
+ },
266
+ isCopied: (child) => child instanceof Element && child.matches("[data-sonic-value]")
267
+ }, signal);
268
+ }
221
269
  clampRange(next) {
222
- return Math.min(this.#max(), Math.max(this.min, next));
270
+ const min = this.min;
271
+ if (!this.wraps()) return clamp(next, min, this.#max());
272
+ const range = this.range();
273
+ if (range <= 0) return min;
274
+ const offset = trimFloat(((next - min) % range + range) % range);
275
+ return offset >= range ? min : trimFloat(min + offset);
276
+ }
277
+ controlRole() {
278
+ return "slider";
223
279
  }
224
280
  fraction(value) {
225
281
  return (this.#taper() ?? linearTaper(this.min, this.#max())).position(value);
@@ -234,11 +290,16 @@ var SonicRangeElement = class extends SonicFormElement {
234
290
  }));
235
291
  return true;
236
292
  }
293
+ modulationFractions(amount) {
294
+ const value = this.fraction(this.#value);
295
+ const modulated = this.fraction(this.#value + amount);
296
+ return [Math.min(value, modulated), Math.max(value, modulated)];
297
+ }
237
298
  originFraction() {
238
- return this.fraction(this.origin ?? this.min);
299
+ return this.fraction(this.restValue());
239
300
  }
240
301
  positions() {
241
- const positions = Math.round(this.range() / this.step) + 1;
302
+ const positions = this.#entries?.length ?? Math.round(this.range() / this.step) + (this.wraps() ? 0 : 1);
242
303
  if (!this.notched || !Number.isFinite(positions) || positions < 2) return;
243
304
  return positions;
244
305
  }
@@ -248,14 +309,15 @@ var SonicRangeElement = class extends SonicFormElement {
248
309
  renderAria(control, orientation) {
249
310
  this.writeFormValue(String(this.#value), String(this.#value));
250
311
  if (this.#entry?.isOpen) {
251
- for (const name of sliderAttributes) control.removeAttribute(name);
312
+ for (const name of roleAttributes) control.removeAttribute(name);
252
313
  this.forwardNaming(control, false);
253
314
  this.#renderReadout();
254
315
  return;
255
316
  }
256
- control.setAttribute("role", "slider");
257
- control.setAttribute("aria-valuemin", String(this.min));
258
- control.setAttribute("aria-valuemax", String(this.#max()));
317
+ const [low, high] = this.#bounds();
318
+ control.setAttribute("role", this.controlRole());
319
+ control.setAttribute("aria-valuemin", String(low));
320
+ control.setAttribute("aria-valuemax", String(high));
259
321
  control.setAttribute("aria-valuenow", String(this.#value));
260
322
  if (orientation) control.setAttribute("aria-orientation", orientation);
261
323
  writeAttribute(control, "aria-valuetext", this.#formatValue?.(this.#value));
@@ -263,28 +325,48 @@ var SonicRangeElement = class extends SonicFormElement {
263
325
  this.#renderDisabled(control);
264
326
  this.#renderReadout();
265
327
  }
328
+ renderScale() {
329
+ const marks = this.#scale?.children ?? [];
330
+ for (const mark of marks) {
331
+ if (!(mark instanceof HTMLElement || mark instanceof SVGElement)) continue;
332
+ const at = String(this.fraction(this.clampRange(scaleValue(mark))));
333
+ if (mark.style.getPropertyValue("--_sonic-scale-at") !== at) mark.style.setProperty("--_sonic-scale-at", at);
334
+ }
335
+ }
266
336
  restoreState(state) {
267
337
  this.value = Number(state);
268
338
  }
339
+ restValue() {
340
+ return this.origin ?? this.#bounds()[0];
341
+ }
342
+ springTarget() {}
269
343
  valueAt(fraction) {
270
344
  return (this.#taper() ?? linearTaper(this.min, this.#max())).value(fraction);
271
345
  }
346
+ valueText() {
347
+ return this.#formatValue?.(this.#value) ?? String(this.#value);
348
+ }
349
+ wraps() {
350
+ return false;
351
+ }
272
352
  #bindEntry(control, signal) {
273
353
  const input = requireChild(control, "input", HTMLInputElement);
274
- const bubble = requireChild(control, "[popover]", HTMLElement);
275
- const anchor = `--sonic-readout-${this.#instance}`;
354
+ const bubble = control.querySelector("[popover]");
276
355
  input.id = `sonic-entry-${this.#instance}`;
277
- this.#readout = {
278
- bubble,
279
- text: requireChild(bubble, "span", HTMLElement)
280
- };
281
- control.style.setProperty("anchor-name", anchor);
282
- bubble.style.setProperty("position-anchor", anchor);
356
+ if (bubble) {
357
+ const anchor = `--sonic-readout-${this.#instance}`;
358
+ this.#readout = {
359
+ bubble,
360
+ text: requireChild(bubble, "span", HTMLElement)
361
+ };
362
+ control.style.setProperty("anchor-name", anchor);
363
+ bubble.style.setProperty("position-anchor", anchor);
364
+ }
283
365
  const entry = new RangeEntry(input, {
284
366
  commit: (text) => {
285
367
  this.#commit((this.#parseValue ?? Number.parseFloat)(text));
286
368
  },
287
- text: () => this.#parseValue ? this.#valueText() : String(this.#value),
369
+ text: () => this.#parseValue ? this.valueText() : String(this.#value),
288
370
  toggle: (isOpen) => {
289
371
  if (isOpen) this.forwardNaming(input, true);
290
372
  this.toggleState("editing", isOpen);
@@ -299,6 +381,7 @@ var SonicRangeElement = class extends SonicFormElement {
299
381
  control.addEventListener("keydown", (event) => {
300
382
  if (this.isDisabled() || event.defaultPrevented || event.target !== control) return;
301
383
  if (event.key === "Enter") {
384
+ if (this.springTarget() !== void 0) return;
302
385
  event.preventDefault();
303
386
  entry.open();
304
387
  return;
@@ -309,19 +392,45 @@ var SonicRangeElement = class extends SonicFormElement {
309
392
  this.#commit(next);
310
393
  this.#revealForKeys();
311
394
  }, { signal });
395
+ control.addEventListener("keyup", (event) => {
396
+ if (keySteps.has(event.key) || endKeys.has(event.key)) this.#springBack();
397
+ }, { signal });
312
398
  control.addEventListener("blur", () => {
313
399
  this.#hideForKeys();
400
+ this.#springBack();
314
401
  }, { signal });
315
402
  }
403
+ #bounds() {
404
+ const entries = this.#entries;
405
+ return [entries?.[0] ?? this.min, entries?.at(-1) ?? this.#max()];
406
+ }
316
407
  #clamp(next) {
408
+ const entries = this.#entries;
409
+ if (entries) return nearestEntry(entries, next);
317
410
  const min = this.min;
318
411
  const step = this.step;
319
412
  const stepped = step > 0 ? min + Math.round((next - min) / step) * step : next;
320
- return this.clampRange(Number(stepped.toPrecision(12)));
413
+ return this.clampRange(trimFloat(stepped));
321
414
  }
322
415
  #commit(next) {
323
416
  if (this.input(next)) this.dispatchEvent(new Event("change", { bubbles: true }));
324
417
  }
418
+ #detentHold(control, travelPx) {
419
+ const detent = this.detent;
420
+ if (detent === void 0) return void 0;
421
+ const value = this.#clamp(detent);
422
+ const zonePx = readPxProperty(getComputedStyle(control), "--_sonic-detent-zone", detentZonePx);
423
+ return {
424
+ place: this.fraction(value),
425
+ slack: value === this.#value ? 0 : void 0,
426
+ value,
427
+ zone: zonePx / travelPx
428
+ };
429
+ }
430
+ #end(step) {
431
+ if (!this.wraps()) return this.#bounds()[1];
432
+ return this.min + (Math.ceil(this.range() / step) - 1) * step;
433
+ }
325
434
  #endDrag() {
326
435
  const drag = this.#drag;
327
436
  if (!drag) return;
@@ -330,6 +439,7 @@ var SonicRangeElement = class extends SonicFormElement {
330
439
  this.toggleState("dragging", false);
331
440
  this.#renderReadout();
332
441
  if (this.#value !== drag.fromValue) this.dispatchEvent(new Event("change", { bubbles: true }));
442
+ this.#springBack();
333
443
  }
334
444
  #hideForKeys() {
335
445
  clearTimeout(this.#keyRevealTimer);
@@ -341,20 +451,29 @@ var SonicRangeElement = class extends SonicFormElement {
341
451
  return (this.#drag?.isRevealed === true || this.#keyRevealTimer !== void 0) && this.readout;
342
452
  }
343
453
  #keyTarget(key) {
344
- if (key === "Home") return this.min;
345
- if (key === "End") return this.#max();
454
+ if (key === "Home") return this.#bounds()[0];
455
+ const step = this.step > 0 ? this.step : this.range() / 100;
456
+ if (key === "End") return this.#end(step);
346
457
  if (resetKeys.has(key)) return this.default;
347
458
  const steps = keySteps.get(key);
348
459
  if (steps === void 0) return void 0;
349
- const step = this.step > 0 ? this.step : this.range() / 100;
350
- const taper = this.#taper();
351
- if (!taper) return this.#value + steps * step;
352
- const next = this.#clamp(taper.value(taper.position(this.#value) + steps / 100));
353
- return next === this.#value ? this.#value + Math.sign(steps) * step : next;
460
+ return this.#stopAtDetent(this.#stepBy(steps, step));
354
461
  }
355
462
  #max() {
356
463
  return Math.max(this.min, this.max);
357
464
  }
465
+ #move(drag, delta) {
466
+ const { detent } = drag;
467
+ const to = drag.rawFraction + delta;
468
+ const next = detent ? passDetent(detent, drag.rawFraction, to) : to;
469
+ if (detent && next === void 0) {
470
+ drag.rawFraction = detent.place;
471
+ return detent.value;
472
+ }
473
+ const place = next ?? to;
474
+ drag.rawFraction = this.wraps() ? wrapUnit(place) : clampUnit(place);
475
+ return this.valueAt(drag.rawFraction);
476
+ }
358
477
  #renderDisabled(control) {
359
478
  if (this.isDisabled()) {
360
479
  control.setAttribute("aria-disabled", "true");
@@ -369,7 +488,7 @@ var SonicRangeElement = class extends SonicFormElement {
369
488
  if (!readout || !canPopover || !this.isConnected) return;
370
489
  const isEditing = this.#entry?.isOpen === true;
371
490
  readout.text.hidden = isEditing;
372
- if (!isEditing) readout.text.textContent = this.#valueText();
491
+ if (!isEditing) readout.text.textContent = this.valueText();
373
492
  readout.bubble.togglePopover(isEditing || this.#isValueShown());
374
493
  }
375
494
  #reset() {
@@ -387,15 +506,32 @@ var SonicRangeElement = class extends SonicFormElement {
387
506
  }, keyRevealMs);
388
507
  this.#renderReadout();
389
508
  }
509
+ #springBack() {
510
+ const target = this.springTarget();
511
+ if (target !== void 0) this.#commit(target);
512
+ }
513
+ #stepBy(steps, step) {
514
+ const entries = this.#entries;
515
+ if (entries) return entries[clamp(entries.indexOf(this.#value) + steps, 0, entries.length - 1)] ?? this.#value;
516
+ const taper = this.#taper();
517
+ if (!taper) return this.#value + steps * step;
518
+ const next = this.#clamp(taper.value(taper.position(this.#value) + steps / 100));
519
+ return next === this.#value ? this.#value + Math.sign(steps) * step : next;
520
+ }
521
+ #stopAtDetent(next) {
522
+ const detent = this.detent;
523
+ if (detent === void 0) return next;
524
+ const value = this.#clamp(detent);
525
+ return (this.#value - value) * (next - value) < 0 ? value : next;
526
+ }
390
527
  #taper() {
391
- if (this.notched) return void 0;
528
+ const entries = this.#entries;
529
+ if (entries) return listTaper(entries);
530
+ if (this.notched || this.wraps()) return void 0;
392
531
  const min = this.min;
393
532
  const max = this.#max();
394
533
  return (this.taper === "log" ? logTaper(min, max) : void 0) ?? skewTaper(min, max, this.midpoint ?? NaN);
395
534
  }
396
- #valueText() {
397
- return this.#formatValue?.(this.#value) ?? String(this.#value);
398
- }
399
535
  #write(next) {
400
536
  if (!Number.isFinite(next)) return;
401
537
  this.#isDirty = true;
@@ -2,10 +2,21 @@ import { requireChild, template } from "../lib/render.js";
2
2
  import { SonicRangeElement } from "./range-element.js";
3
3
 
4
4
  //#region src/elements/slider.ts
5
+ function readRanges(ranges) {
6
+ if ("start" in ranges) return Array.from({ length: ranges.length }, (_entry, index) => [ranges.start(index), ranges.end(index)]);
7
+ return [...ranges].map(([start, end]) => [start, end]);
8
+ }
9
+ function grooveStop(fraction) {
10
+ if (fraction <= 0) return "0%";
11
+ if (fraction >= 1) return "100%";
12
+ return `calc(${String(fraction)} * var(--_sonic-slider-travel) + var(--_sonic-slider-groove) / 2)`;
13
+ }
5
14
  const renderSlider = template(`
6
15
  <div class="sonic-slider" role="slider" tabindex="0">
16
+ <div class="sonic-slider-scale" aria-hidden="true"></div>
7
17
  <div class="sonic-slider-marks"></div>
8
18
  <div class="sonic-slider-groove"></div>
19
+ <div class="sonic-slider-modulation"></div>
9
20
  <div class="sonic-slider-cap"></div>
10
21
  <div class="sonic-slider-readout" popover="manual">
11
22
  <span></span>
@@ -21,28 +32,53 @@ const renderSlider = template(`
21
32
  </div>
22
33
  `, HTMLDivElement);
23
34
  var SonicSlider = class extends SonicRangeElement {
24
- static observedAttributes = [...SonicRangeElement.observedAttributes, "orientation"];
35
+ static observedAttributes = [
36
+ ...SonicRangeElement.observedAttributes,
37
+ "modulation",
38
+ "orientation",
39
+ "spring"
40
+ ];
41
+ get buffered() {
42
+ return this.#buffered.map(([start, end]) => [start, end]);
43
+ }
44
+ set buffered(ranges) {
45
+ this.#buffered = ranges ? readRanges(ranges).toSorted((first, second) => first[0] - second[0]) : [];
46
+ this.render();
47
+ }
25
48
  get groovePress() {
26
49
  return this.getAttribute("groove-press") === "none" ? "none" : "jump";
27
50
  }
28
51
  set groovePress(gesture) {
29
52
  this.reflect("groove-press", gesture);
30
53
  }
54
+ get modulation() {
55
+ return this.numberAttribute("modulation", 0);
56
+ }
57
+ set modulation(amount) {
58
+ this.reflect("modulation", amount);
59
+ }
31
60
  get orientation() {
32
61
  return this.getAttribute("orientation") === "vertical" ? "vertical" : "horizontal";
33
62
  }
34
63
  set orientation(direction) {
35
64
  this.reflect("orientation", direction);
36
65
  }
66
+ get spring() {
67
+ return this.hasAttribute("spring");
68
+ }
69
+ set spring(isSpring) {
70
+ this.reflect("spring", isSpring);
71
+ }
72
+ #buffered = [];
37
73
  #slider = renderSlider();
38
74
  connectedCallback() {
39
- this.upgradeProperties("groovePress", "orientation");
75
+ this.upgradeProperties("buffered", "groovePress", "modulation", "orientation", "spring");
40
76
  super.connectedCallback();
41
77
  }
42
78
  connect(signal) {
43
79
  const slider = this.#slider;
44
80
  const cap = requireChild(slider, ".sonic-slider-cap", HTMLDivElement);
45
- this.appendOnce(slider);
81
+ this.bindScale(slider, requireChild(slider, ".sonic-slider-scale", HTMLDivElement), signal);
46
82
  this.render();
47
83
  this.checkStyles(slider, "slider.css");
48
84
  this.bindGestures(slider, signal, (event) => {
@@ -59,12 +95,20 @@ var SonicSlider = class extends SonicRangeElement {
59
95
  render() {
60
96
  const slider = this.#slider;
61
97
  const positions = this.positions();
98
+ const [modulationFrom, modulationTo] = this.modulationFractions(this.modulation);
62
99
  slider.style.setProperty("--_sonic-slider-value", String(this.fraction(this.value)));
63
100
  slider.style.setProperty("--_sonic-slider-origin", String(this.originFraction()));
101
+ slider.style.setProperty("--_sonic-slider-modulation-from", String(modulationFrom));
102
+ slider.style.setProperty("--_sonic-slider-modulation-to", String(modulationTo));
64
103
  if (positions === void 0) slider.style.removeProperty("--_sonic-slider-positions");
65
104
  else slider.style.setProperty("--_sonic-slider-positions", String(positions));
105
+ this.#renderBuffered(slider);
106
+ this.renderScale();
66
107
  this.renderAria(slider, this.orientation);
67
108
  }
109
+ springTarget() {
110
+ return this.spring ? this.restValue() : void 0;
111
+ }
68
112
  #axis(slider, cap) {
69
113
  const track = slider.getBoundingClientRect();
70
114
  const capBox = cap.getBoundingClientRect();
@@ -79,6 +123,22 @@ var SonicSlider = class extends SonicRangeElement {
79
123
  travelPx: Math.max(1, track.width - capBox.width)
80
124
  };
81
125
  }
126
+ #renderBuffered(slider) {
127
+ if (this.#buffered.length === 0) {
128
+ slider.style.removeProperty("--_sonic-slider-buffered-ranges");
129
+ return;
130
+ }
131
+ const stops = this.#buffered.flatMap(([start, end]) => {
132
+ const from = grooveStop(this.fraction(Math.min(start, end)));
133
+ const to = grooveStop(this.fraction(Math.max(start, end)));
134
+ return [
135
+ `transparent ${from}`,
136
+ `var(--_sonic-slider-buffered) ${from} ${to}`,
137
+ `transparent ${to}`
138
+ ];
139
+ });
140
+ slider.style.setProperty("--_sonic-slider-buffered-ranges", `linear-gradient(var(--_sonic-slider-toward), ${stops.join(", ")})`);
141
+ }
82
142
  };
83
143
 
84
144
  //#endregion
@@ -1,8 +1,9 @@
1
1
  import { SonicDial } from "./elements/dial.js";
2
2
  import { SonicKey } from "./elements/key.js";
3
3
  import { SonicMeter } from "./elements/meter.js";
4
+ import { SonicNumber } from "./elements/number.js";
4
5
  import { SonicSegmented } from "./elements/segmented.js";
5
6
  import { SonicSlider } from "./elements/slider.js";
6
7
  import { formatPercent, parsePercent } from "./lib/percent.js";
7
8
 
8
- export { SonicDial, SonicKey, SonicMeter, SonicSegmented, SonicSlider, formatPercent, parsePercent };
9
+ export { SonicDial, SonicKey, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, formatPercent, parsePercent };
@@ -0,0 +1,17 @@
1
+ //#region src/lib/detent.ts
2
+ function isAcross(from, to, place) {
3
+ return from !== place && (from - place) * (to - place) <= 0;
4
+ }
5
+ function passDetent(hold, from, to) {
6
+ if (hold.slack === void 0 && !isAcross(from, to, hold.place)) return to;
7
+ const slack = (hold.slack ?? 0) + to - hold.place;
8
+ if (Math.abs(slack) <= hold.zone) {
9
+ hold.slack = slack;
10
+ return;
11
+ }
12
+ hold.slack = void 0;
13
+ return hold.place + slack - Math.sign(slack) * hold.zone;
14
+ }
15
+
16
+ //#endregion
17
+ export { passDetent };
@@ -0,0 +1,16 @@
1
+ //#region src/lib/math.ts
2
+ function clamp(value, min, max) {
3
+ return Math.min(max, Math.max(min, value));
4
+ }
5
+ function clampUnit(fraction) {
6
+ return clamp(fraction, 0, 1);
7
+ }
8
+ function wrapUnit(fraction) {
9
+ return fraction - Math.floor(fraction);
10
+ }
11
+ function trimFloat(value) {
12
+ return Number(value.toPrecision(12));
13
+ }
14
+
15
+ //#endregion
16
+ export { clamp, clampUnit, trimFloat, wrapUnit };
@@ -0,0 +1,14 @@
1
+ //#region src/lib/number-list.ts
2
+ function parseNumberList(text) {
3
+ if (text === null) return void 0;
4
+ const entries = [...new Set(text.split(/[\s,]+/).filter(Boolean).map(Number).filter((entry) => Number.isFinite(entry)))].toSorted((first, second) => first - second);
5
+ return entries.length >= 2 ? entries : void 0;
6
+ }
7
+ function nearestEntry(entries, value) {
8
+ let nearest = entries[0] ?? value;
9
+ for (const entry of entries) if (Math.abs(entry - value) < Math.abs(nearest - value)) nearest = entry;
10
+ return nearest;
11
+ }
12
+
13
+ //#endregion
14
+ export { nearestEntry, parseNumberList };
@@ -1,7 +1,6 @@
1
+ import { clampUnit } from "./math.js";
2
+
1
3
  //#region src/lib/taper.ts
2
- function clampUnit(fraction) {
3
- return Math.min(1, Math.max(0, fraction));
4
- }
5
4
  function linearTaper(min, max) {
6
5
  const range = max - min;
7
6
  return {
@@ -27,6 +26,20 @@ function skewTaper(min, max, midpoint) {
27
26
  value: (position) => min + range * clampUnit(position) ** (1 / skew)
28
27
  };
29
28
  }
29
+ function listTaper(entries) {
30
+ const last = entries.length - 1;
31
+ return {
32
+ position: (value) => {
33
+ const above = entries.findIndex((entry) => entry >= value);
34
+ if (above === -1) return 1;
35
+ const high = entries[above];
36
+ const low = entries[above - 1];
37
+ if (high === void 0 || low === void 0) return 0;
38
+ return (above - 1 + (value - low) / (high - low)) / last;
39
+ },
40
+ value: (position) => entries[Math.round(clampUnit(position) * last)] ?? NaN
41
+ };
42
+ }
30
43
 
31
44
  //#endregion
32
- export { clampUnit, linearTaper, logTaper, skewTaper };
45
+ export { linearTaper, listTaper, logTaper, skewTaper };
@@ -0,0 +1,2 @@
1
+ import { SonicNumber } from "../elements/number.js";
2
+ export type { SonicNumber };
@@ -0,0 +1,8 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicNumber } from "../elements/number.js";
3
+
4
+ //#region src/define/number.ts
5
+ defineOnce("sonic-number", SonicNumber);
6
+
7
+ //#endregion
8
+ //# sourceMappingURL=number.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number.js","names":[],"sources":["../../../src/define/number.ts"],"sourcesContent":["import { defineOnce } from '#elements/define-once.ts';\nimport { SonicNumber } from '#elements/number.ts';\n\ndefineOnce('sonic-number', SonicNumber);\n\n// Type-only, so the tag map augmentation ships without a runtime export\n/** @public */\nexport type { SonicNumber } from '#elements/number.ts';\n"],"mappings":";;;;AAGA,WAAW,gBAAgB,WAAW"}
@@ -4,8 +4,10 @@ import { SonicKey } from "./elements/key.js";
4
4
  import "./define/key.js";
5
5
  import { SonicMeter } from "./elements/meter.js";
6
6
  import "./define/meter.js";
7
+ import { SonicNumber } from "./elements/number.js";
8
+ import "./define/number.js";
7
9
  import { SonicSegmented } from "./elements/segmented.js";
8
10
  import "./define/segmented.js";
9
11
  import { SonicSlider } from "./elements/slider.js";
10
12
  import "./define/slider.js";
11
- export type { SonicDial, SonicKey, SonicMeter, SonicSegmented, SonicSlider };
13
+ export type { SonicDial, SonicKey, SonicMeter, SonicNumber, SonicSegmented, SonicSlider };
@@ -1,5 +1,6 @@
1
1
  import "./define/dial.js";
2
2
  import "./define/key.js";
3
3
  import "./define/meter.js";
4
+ import "./define/number.js";
4
5
  import "./define/segmented.js";
5
6
  import "./define/slider.js";