@xsynaptic/sonic-ui 0.2.0 → 0.3.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 (42) 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 +19 -7
  5. package/dist/default/elements/number.js +46 -0
  6. package/dist/default/elements/range-element.js +46 -23
  7. package/dist/default/elements/slider.js +16 -2
  8. package/dist/default/index.js +2 -1
  9. package/dist/default/lib/taper.js +4 -1
  10. package/dist/dev/define/number.d.ts +2 -0
  11. package/dist/dev/define/number.js +8 -0
  12. package/dist/dev/define/number.js.map +1 -0
  13. package/dist/dev/define.d.ts +3 -1
  14. package/dist/dev/define.js +1 -0
  15. package/dist/dev/elements/dial.d.ts +3 -0
  16. package/dist/dev/elements/dial.js +19 -7
  17. package/dist/dev/elements/dial.js.map +1 -1
  18. package/dist/dev/elements/number.d.ts +16 -0
  19. package/dist/dev/elements/number.js +47 -0
  20. package/dist/dev/elements/number.js.map +1 -0
  21. package/dist/dev/elements/range-element.d.ts +4 -0
  22. package/dist/dev/elements/range-element.js +46 -23
  23. package/dist/dev/elements/range-element.js.map +1 -1
  24. package/dist/dev/elements/slider.d.ts +2 -0
  25. package/dist/dev/elements/slider.js +16 -2
  26. package/dist/dev/elements/slider.js.map +1 -1
  27. package/dist/dev/index.d.ts +2 -1
  28. package/dist/dev/index.js +2 -1
  29. package/dist/dev/lib/read-px-property.js.map +1 -1
  30. package/dist/dev/lib/taper.js +4 -1
  31. package/dist/dev/lib/taper.js.map +1 -1
  32. package/dist/styles/controls.css +2 -0
  33. package/dist/styles/dial.css +37 -3
  34. package/dist/styles/key.css +41 -1
  35. package/dist/styles/led.css +75 -0
  36. package/dist/styles/material.css +100 -19
  37. package/dist/styles/meter.css +19 -3
  38. package/dist/styles/number.css +81 -0
  39. package/dist/styles/screen.css +6 -39
  40. package/dist/styles/segmented.css +21 -0
  41. package/dist/styles/slider.css +52 -1
  42. package/package.json +4 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @xsynaptic/sonic-ui
2
2
 
3
- Native custom elements for audio interfaces: dials, sliders, keys, a segmented switch, a peak meter and a screen panel. Each control renders in the light DOM, drawn in CSS, skinned with `--sonic-*` custom properties. No framework, no runtime dependencies.
3
+ Native custom elements for audio interfaces. Each control renders in the light DOM, drawn in CSS, skinned with `--sonic-*` custom properties. No framework, no runtime dependencies.
4
4
 
5
5
  Early and unstable: any 0.x release may break the API.
6
6
 
@@ -34,7 +34,7 @@ The styles sit in `@layer sonic`, so list it after your resets. A skin (`slate`,
34
34
  - **Attributes and properties** reflect each other: `dial.max = 200` writes `max="200"`. `value` and the key's `pressed` are the live state and never write their attribute, as on native inputs.
35
35
  - **Events:** `change` when a control settles, and `input` while a dial or slider moves; both bubble.
36
36
  - **Formatting:** `formatValue` sets the text the readout and assistive technology read, and `parseValue` reads typed entry back. `formatPercent` and `parsePercent` from the package root show 0 to 1 as a percentage.
37
- - **Forms:** the dial, slider, key and switch submit under their `name`, reset, take a `<label>`, and follow a disabled `<fieldset>`, as native inputs do.
37
+ - **Forms:** controls that hold a value submit under their `name`, reset, take a `<label>`, and follow a disabled `<fieldset>`, as native inputs do.
38
38
 
39
39
  ## Browsers
40
40
 
@@ -0,0 +1,7 @@
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
@@ -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";
@@ -25,7 +25,17 @@ const renderDial = template(`
25
25
  `, HTMLDivElement);
26
26
  const fallbackTravelPx = 160;
27
27
  var SonicDial = class extends SonicRangeElement {
28
- static observedAttributes = [...SonicRangeElement.observedAttributes, "modulation"];
28
+ static observedAttributes = [
29
+ ...SonicRangeElement.observedAttributes,
30
+ "endless",
31
+ "modulation"
32
+ ];
33
+ get endless() {
34
+ return this.hasAttribute("endless");
35
+ }
36
+ set endless(isEndless) {
37
+ this.reflect("endless", isEndless);
38
+ }
29
39
  get modulation() {
30
40
  return this.numberAttribute("modulation", 0);
31
41
  }
@@ -34,7 +44,7 @@ var SonicDial = class extends SonicRangeElement {
34
44
  }
35
45
  #dial = renderDial();
36
46
  connectedCallback() {
37
- this.upgradeProperties("modulation");
47
+ this.upgradeProperties("endless", "modulation");
38
48
  super.connectedCallback();
39
49
  }
40
50
  connect(signal) {
@@ -53,16 +63,18 @@ var SonicDial = class extends SonicRangeElement {
53
63
  render() {
54
64
  const dial = this.#dial;
55
65
  const positions = this.positions();
56
- const modulated = this.fraction(this.value + this.modulation);
57
- const value = this.fraction(this.value);
58
- dial.style.setProperty("--_sonic-dial-value", String(value));
66
+ const [modulationFrom, modulationTo] = this.modulationFractions(this.endless ? 0 : this.modulation);
67
+ dial.style.setProperty("--_sonic-dial-value", String(this.fraction(this.value)));
59
68
  dial.style.setProperty("--_sonic-dial-origin", String(this.originFraction()));
60
- dial.style.setProperty("--_sonic-dial-modulation-from", String(Math.min(value, modulated)));
61
- dial.style.setProperty("--_sonic-dial-modulation-to", String(Math.max(value, modulated)));
69
+ dial.style.setProperty("--_sonic-dial-modulation-from", String(modulationFrom));
70
+ dial.style.setProperty("--_sonic-dial-modulation-to", String(modulationTo));
62
71
  if (positions === void 0) dial.style.removeProperty("--_sonic-dial-positions");
63
72
  else dial.style.setProperty("--_sonic-dial-positions", String(positions));
64
73
  this.renderAria(dial);
65
74
  }
75
+ wraps() {
76
+ return this.endless;
77
+ }
66
78
  };
67
79
 
68
80
  //#endregion
@@ -0,0 +1,46 @@
1
+ import { requireChild, template } from "../lib/render.js";
2
+ import { SonicRangeElement } from "./range-element.js";
3
+ import { readPxProperty } from "../lib/read-px-property.js";
4
+
5
+ //#region src/elements/number.ts
6
+ const renderNumber = template(`
7
+ <div class="sonic-number" role="spinbutton" tabindex="0">
8
+ <span aria-hidden="true" class="sonic-number-value"></span>
9
+ <input
10
+ autocomplete="off"
11
+ class="sonic-number-entry"
12
+ enterkeyhint="done"
13
+ hidden
14
+ spellcheck="false"
15
+ type="text"
16
+ />
17
+ </div>
18
+ `, HTMLDivElement);
19
+ const fallbackTravelPx = 160;
20
+ var SonicNumber = class extends SonicRangeElement {
21
+ #number = renderNumber();
22
+ #digits = requireChild(this.#number, ".sonic-number-value", HTMLSpanElement);
23
+ connect(signal) {
24
+ const number = this.#number;
25
+ this.appendOnce(number);
26
+ this.render();
27
+ this.checkStyles(number, "number.css");
28
+ this.bindGestures(number, signal, () => ({
29
+ position: (event) => -event.clientY,
30
+ travelPx: readPxProperty(getComputedStyle(number), "--_sonic-number-travel", fallbackTravelPx)
31
+ }));
32
+ }
33
+ controlRole() {
34
+ return "spinbutton";
35
+ }
36
+ focusTarget() {
37
+ return this.#number;
38
+ }
39
+ render() {
40
+ this.#digits.textContent = this.valueText();
41
+ this.renderAria(this.#number);
42
+ }
43
+ };
44
+
45
+ //#endregion
46
+ export { SonicNumber };
@@ -2,7 +2,7 @@ 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 { clampUnit, linearTaper, logTaper, skewTaper, wrapUnit } from "../lib/taper.js";
6
6
 
7
7
  //#region src/elements/range-element.ts
8
8
  const fineScale = .1;
@@ -21,7 +21,7 @@ const keySteps = /* @__PURE__ */ new Map([
21
21
  ["PageDown", -10],
22
22
  ["PageUp", pageSteps]
23
23
  ]);
24
- const sliderAttributes = [
24
+ const roleAttributes = [
25
25
  "aria-disabled",
26
26
  "aria-orientation",
27
27
  "aria-valuemax",
@@ -192,7 +192,8 @@ var SonicRangeElement = class extends SonicFormElement {
192
192
  if (event.pointerId !== drag?.pointerId) return;
193
193
  const position = drag.position(event);
194
194
  const scale = event.shiftKey ? fineScale : 1;
195
- drag.rawFraction = clampUnit(drag.rawFraction + (position - drag.lastPosition) / drag.travelPx * scale);
195
+ const fraction = drag.rawFraction + (position - drag.lastPosition) / drag.travelPx * scale;
196
+ drag.rawFraction = this.wraps() ? wrapUnit(fraction) : clampUnit(fraction);
196
197
  drag.lastPosition = position;
197
198
  if (Math.abs(position - drag.startPosition) >= revealPx) this.#reveal(drag);
198
199
  this.input(this.valueAt(drag.rawFraction));
@@ -219,7 +220,15 @@ var SonicRangeElement = class extends SonicFormElement {
219
220
  }, { once: true });
220
221
  }
221
222
  clampRange(next) {
222
- return Math.min(this.#max(), Math.max(this.min, next));
223
+ const min = this.min;
224
+ if (!this.wraps()) return Math.min(this.#max(), Math.max(min, next));
225
+ const range = this.range();
226
+ if (range <= 0) return min;
227
+ const offset = Number((((next - min) % range + range) % range).toPrecision(12));
228
+ return offset >= range ? min : Number((min + offset).toPrecision(12));
229
+ }
230
+ controlRole() {
231
+ return "slider";
223
232
  }
224
233
  fraction(value) {
225
234
  return (this.#taper() ?? linearTaper(this.min, this.#max())).position(value);
@@ -234,11 +243,16 @@ var SonicRangeElement = class extends SonicFormElement {
234
243
  }));
235
244
  return true;
236
245
  }
246
+ modulationFractions(amount) {
247
+ const value = this.fraction(this.#value);
248
+ const modulated = this.fraction(this.#value + amount);
249
+ return [Math.min(value, modulated), Math.max(value, modulated)];
250
+ }
237
251
  originFraction() {
238
252
  return this.fraction(this.origin ?? this.min);
239
253
  }
240
254
  positions() {
241
- const positions = Math.round(this.range() / this.step) + 1;
255
+ const positions = Math.round(this.range() / this.step) + (this.wraps() ? 0 : 1);
242
256
  if (!this.notched || !Number.isFinite(positions) || positions < 2) return;
243
257
  return positions;
244
258
  }
@@ -248,12 +262,12 @@ var SonicRangeElement = class extends SonicFormElement {
248
262
  renderAria(control, orientation) {
249
263
  this.writeFormValue(String(this.#value), String(this.#value));
250
264
  if (this.#entry?.isOpen) {
251
- for (const name of sliderAttributes) control.removeAttribute(name);
265
+ for (const name of roleAttributes) control.removeAttribute(name);
252
266
  this.forwardNaming(control, false);
253
267
  this.#renderReadout();
254
268
  return;
255
269
  }
256
- control.setAttribute("role", "slider");
270
+ control.setAttribute("role", this.controlRole());
257
271
  control.setAttribute("aria-valuemin", String(this.min));
258
272
  control.setAttribute("aria-valuemax", String(this.#max()));
259
273
  control.setAttribute("aria-valuenow", String(this.#value));
@@ -269,22 +283,30 @@ var SonicRangeElement = class extends SonicFormElement {
269
283
  valueAt(fraction) {
270
284
  return (this.#taper() ?? linearTaper(this.min, this.#max())).value(fraction);
271
285
  }
286
+ valueText() {
287
+ return this.#formatValue?.(this.#value) ?? String(this.#value);
288
+ }
289
+ wraps() {
290
+ return false;
291
+ }
272
292
  #bindEntry(control, signal) {
273
293
  const input = requireChild(control, "input", HTMLInputElement);
274
- const bubble = requireChild(control, "[popover]", HTMLElement);
275
- const anchor = `--sonic-readout-${this.#instance}`;
294
+ const bubble = control.querySelector("[popover]");
276
295
  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);
296
+ if (bubble) {
297
+ const anchor = `--sonic-readout-${this.#instance}`;
298
+ this.#readout = {
299
+ bubble,
300
+ text: requireChild(bubble, "span", HTMLElement)
301
+ };
302
+ control.style.setProperty("anchor-name", anchor);
303
+ bubble.style.setProperty("position-anchor", anchor);
304
+ }
283
305
  const entry = new RangeEntry(input, {
284
306
  commit: (text) => {
285
307
  this.#commit((this.#parseValue ?? Number.parseFloat)(text));
286
308
  },
287
- text: () => this.#parseValue ? this.#valueText() : String(this.#value),
309
+ text: () => this.#parseValue ? this.valueText() : String(this.#value),
288
310
  toggle: (isOpen) => {
289
311
  if (isOpen) this.forwardNaming(input, true);
290
312
  this.toggleState("editing", isOpen);
@@ -322,6 +344,10 @@ var SonicRangeElement = class extends SonicFormElement {
322
344
  #commit(next) {
323
345
  if (this.input(next)) this.dispatchEvent(new Event("change", { bubbles: true }));
324
346
  }
347
+ #end(step) {
348
+ if (!this.wraps()) return this.#max();
349
+ return this.min + (Math.ceil(this.range() / step) - 1) * step;
350
+ }
325
351
  #endDrag() {
326
352
  const drag = this.#drag;
327
353
  if (!drag) return;
@@ -342,11 +368,11 @@ var SonicRangeElement = class extends SonicFormElement {
342
368
  }
343
369
  #keyTarget(key) {
344
370
  if (key === "Home") return this.min;
345
- if (key === "End") return this.#max();
371
+ const step = this.step > 0 ? this.step : this.range() / 100;
372
+ if (key === "End") return this.#end(step);
346
373
  if (resetKeys.has(key)) return this.default;
347
374
  const steps = keySteps.get(key);
348
375
  if (steps === void 0) return void 0;
349
- const step = this.step > 0 ? this.step : this.range() / 100;
350
376
  const taper = this.#taper();
351
377
  if (!taper) return this.#value + steps * step;
352
378
  const next = this.#clamp(taper.value(taper.position(this.#value) + steps / 100));
@@ -369,7 +395,7 @@ var SonicRangeElement = class extends SonicFormElement {
369
395
  if (!readout || !canPopover || !this.isConnected) return;
370
396
  const isEditing = this.#entry?.isOpen === true;
371
397
  readout.text.hidden = isEditing;
372
- if (!isEditing) readout.text.textContent = this.#valueText();
398
+ if (!isEditing) readout.text.textContent = this.valueText();
373
399
  readout.bubble.togglePopover(isEditing || this.#isValueShown());
374
400
  }
375
401
  #reset() {
@@ -388,14 +414,11 @@ var SonicRangeElement = class extends SonicFormElement {
388
414
  this.#renderReadout();
389
415
  }
390
416
  #taper() {
391
- if (this.notched) return void 0;
417
+ if (this.notched || this.wraps()) return void 0;
392
418
  const min = this.min;
393
419
  const max = this.#max();
394
420
  return (this.taper === "log" ? logTaper(min, max) : void 0) ?? skewTaper(min, max, this.midpoint ?? NaN);
395
421
  }
396
- #valueText() {
397
- return this.#formatValue?.(this.#value) ?? String(this.#value);
398
- }
399
422
  #write(next) {
400
423
  if (!Number.isFinite(next)) return;
401
424
  this.#isDirty = true;
@@ -6,6 +6,7 @@ const renderSlider = template(`
6
6
  <div class="sonic-slider" role="slider" tabindex="0">
7
7
  <div class="sonic-slider-marks"></div>
8
8
  <div class="sonic-slider-groove"></div>
9
+ <div class="sonic-slider-modulation"></div>
9
10
  <div class="sonic-slider-cap"></div>
10
11
  <div class="sonic-slider-readout" popover="manual">
11
12
  <span></span>
@@ -21,13 +22,23 @@ const renderSlider = template(`
21
22
  </div>
22
23
  `, HTMLDivElement);
23
24
  var SonicSlider = class extends SonicRangeElement {
24
- static observedAttributes = [...SonicRangeElement.observedAttributes, "orientation"];
25
+ static observedAttributes = [
26
+ ...SonicRangeElement.observedAttributes,
27
+ "modulation",
28
+ "orientation"
29
+ ];
25
30
  get groovePress() {
26
31
  return this.getAttribute("groove-press") === "none" ? "none" : "jump";
27
32
  }
28
33
  set groovePress(gesture) {
29
34
  this.reflect("groove-press", gesture);
30
35
  }
36
+ get modulation() {
37
+ return this.numberAttribute("modulation", 0);
38
+ }
39
+ set modulation(amount) {
40
+ this.reflect("modulation", amount);
41
+ }
31
42
  get orientation() {
32
43
  return this.getAttribute("orientation") === "vertical" ? "vertical" : "horizontal";
33
44
  }
@@ -36,7 +47,7 @@ var SonicSlider = class extends SonicRangeElement {
36
47
  }
37
48
  #slider = renderSlider();
38
49
  connectedCallback() {
39
- this.upgradeProperties("groovePress", "orientation");
50
+ this.upgradeProperties("groovePress", "modulation", "orientation");
40
51
  super.connectedCallback();
41
52
  }
42
53
  connect(signal) {
@@ -59,8 +70,11 @@ var SonicSlider = class extends SonicRangeElement {
59
70
  render() {
60
71
  const slider = this.#slider;
61
72
  const positions = this.positions();
73
+ const [modulationFrom, modulationTo] = this.modulationFractions(this.modulation);
62
74
  slider.style.setProperty("--_sonic-slider-value", String(this.fraction(this.value)));
63
75
  slider.style.setProperty("--_sonic-slider-origin", String(this.originFraction()));
76
+ slider.style.setProperty("--_sonic-slider-modulation-from", String(modulationFrom));
77
+ slider.style.setProperty("--_sonic-slider-modulation-to", String(modulationTo));
64
78
  if (positions === void 0) slider.style.removeProperty("--_sonic-slider-positions");
65
79
  else slider.style.setProperty("--_sonic-slider-positions", String(positions));
66
80
  this.renderAria(slider, this.orientation);
@@ -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 };
@@ -2,6 +2,9 @@
2
2
  function clampUnit(fraction) {
3
3
  return Math.min(1, Math.max(0, fraction));
4
4
  }
5
+ function wrapUnit(fraction) {
6
+ return fraction - Math.floor(fraction);
7
+ }
5
8
  function linearTaper(min, max) {
6
9
  const range = max - min;
7
10
  return {
@@ -29,4 +32,4 @@ function skewTaper(min, max, midpoint) {
29
32
  }
30
33
 
31
34
  //#endregion
32
- export { clampUnit, linearTaper, logTaper, skewTaper };
35
+ export { clampUnit, linearTaper, logTaper, skewTaper, wrapUnit };
@@ -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";
@@ -8,12 +8,15 @@ declare global {
8
8
  export declare class SonicDial extends SonicRangeElement {
9
9
  #private;
10
10
  static readonly observedAttributes: string[];
11
+ get endless(): boolean;
12
+ set endless(isEndless: boolean);
11
13
  get modulation(): number;
12
14
  set modulation(amount: number | undefined);
13
15
  connectedCallback(): void;
14
16
  protected connect(signal: AbortSignal): void;
15
17
  protected focusTarget(): HTMLElement;
16
18
  protected render(): void;
19
+ protected wraps(): boolean;
17
20
  }
18
21
  //#endregion
19
22
  //# sourceMappingURL=dial.d.ts.map
@@ -25,7 +25,17 @@ const renderDial = template(`
25
25
  `, HTMLDivElement);
26
26
  const fallbackTravelPx = 160;
27
27
  var SonicDial = class extends SonicRangeElement {
28
- static observedAttributes = [...SonicRangeElement.observedAttributes, "modulation"];
28
+ static observedAttributes = [
29
+ ...SonicRangeElement.observedAttributes,
30
+ "endless",
31
+ "modulation"
32
+ ];
33
+ get endless() {
34
+ return this.hasAttribute("endless");
35
+ }
36
+ set endless(isEndless) {
37
+ this.reflect("endless", isEndless);
38
+ }
29
39
  get modulation() {
30
40
  return this.numberAttribute("modulation", 0);
31
41
  }
@@ -34,7 +44,7 @@ var SonicDial = class extends SonicRangeElement {
34
44
  }
35
45
  #dial = renderDial();
36
46
  connectedCallback() {
37
- this.upgradeProperties("modulation");
47
+ this.upgradeProperties("endless", "modulation");
38
48
  super.connectedCallback();
39
49
  }
40
50
  connect(signal) {
@@ -53,16 +63,18 @@ var SonicDial = class extends SonicRangeElement {
53
63
  render() {
54
64
  const dial = this.#dial;
55
65
  const positions = this.positions();
56
- const modulated = this.fraction(this.value + this.modulation);
57
- const value = this.fraction(this.value);
58
- dial.style.setProperty("--_sonic-dial-value", String(value));
66
+ const [modulationFrom, modulationTo] = this.modulationFractions(this.endless ? 0 : this.modulation);
67
+ dial.style.setProperty("--_sonic-dial-value", String(this.fraction(this.value)));
59
68
  dial.style.setProperty("--_sonic-dial-origin", String(this.originFraction()));
60
- dial.style.setProperty("--_sonic-dial-modulation-from", String(Math.min(value, modulated)));
61
- dial.style.setProperty("--_sonic-dial-modulation-to", String(Math.max(value, modulated)));
69
+ dial.style.setProperty("--_sonic-dial-modulation-from", String(modulationFrom));
70
+ dial.style.setProperty("--_sonic-dial-modulation-to", String(modulationTo));
62
71
  if (positions === void 0) dial.style.removeProperty("--_sonic-dial-positions");
63
72
  else dial.style.setProperty("--_sonic-dial-positions", String(positions));
64
73
  this.renderAria(dial);
65
74
  }
75
+ wraps() {
76
+ return this.endless;
77
+ }
66
78
  };
67
79
 
68
80
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"dial.js","names":[],"sources":["../../../src/elements/dial.ts"],"sourcesContent":["import { SonicRangeElement } from '#elements/range-element.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-dial': SonicDial;\n\t}\n}\n\nconst renderDial = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-dial\" role=\"slider\" tabindex=\"0\">\n\t\t\t<div class=\"sonic-dial-notches\"></div>\n\t\t\t<div class=\"sonic-dial-ring\"></div>\n\t\t\t<div class=\"sonic-dial-modulation\"></div>\n\t\t\t<div class=\"sonic-dial-cap\"></div>\n\t\t\t<div class=\"sonic-dial-pointer\"></div>\n\t\t\t<div class=\"sonic-dial-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-dial-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst fallbackTravelPx = 160;\n\nexport class SonicDial extends SonicRangeElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicRangeElement.observedAttributes,\n\t\t'modulation',\n\t];\n\n\tget modulation(): number {\n\t\treturn this.numberAttribute('modulation', 0);\n\t}\n\n\tset modulation(amount: number | undefined) {\n\t\tthis.reflect('modulation', amount);\n\t}\n\n\treadonly #dial = renderDial();\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('modulation');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst dial = this.#dial;\n\n\t\tthis.appendOnce(dial);\n\t\tthis.render();\n\t\tthis.checkStyles(dial, 'dial.css');\n\t\tthis.bindGestures(dial, signal, () => ({\n\t\t\tposition: (event) => -event.clientY,\n\t\t\t// Once per gesture; a computed style read per move forces a style recalc\n\t\t\ttravelPx: readPxProperty(getComputedStyle(dial), '--_sonic-dial-travel', fallbackTravelPx),\n\t\t}));\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#dial;\n\t}\n\n\tprotected render(): void {\n\t\tconst dial = this.#dial;\n\t\tconst positions = this.positions();\n\t\tconst modulated = this.fraction(this.value + this.modulation);\n\t\tconst value = this.fraction(this.value);\n\n\t\tdial.style.setProperty('--_sonic-dial-value', String(value));\n\t\tdial.style.setProperty('--_sonic-dial-origin', String(this.originFraction()));\n\t\tdial.style.setProperty('--_sonic-dial-modulation-from', String(Math.min(value, modulated)));\n\t\tdial.style.setProperty('--_sonic-dial-modulation-to', String(Math.max(value, modulated)));\n\t\tif (positions === undefined) dial.style.removeProperty('--_sonic-dial-positions');\n\t\telse dial.style.setProperty('--_sonic-dial-positions', String(positions));\n\t\tthis.renderAria(dial);\n\t}\n}\n"],"mappings":";;;;;AAUA,MAAM,aAAa,SACP;;;;;;;;;;;;;;;;;;;IAoBX,cACD;AAEA,MAAM,mBAAmB;AAEzB,IAAa,YAAb,cAA+B,kBAAkB;CAChD,OAAyB,qBAAqB,CAC7C,GAAG,kBAAkB,oBACrB,YACD;CAEA,IAAI,aAAqB;EACxB,OAAO,KAAK,gBAAgB,cAAc,CAAC;CAC5C;CAEA,IAAI,WAAW,QAA4B;EAC1C,KAAK,QAAQ,cAAc,MAAM;CAClC;CAEA,AAAS,QAAQ,WAAW;CAE5B,AAAS,oBAA0B;EAClC,KAAK,kBAAkB,YAAY;EACnC,MAAM,kBAAkB;CACzB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,OAAO,KAAK;EAElB,KAAK,WAAW,IAAI;EACpB,KAAK,OAAO;EACZ,KAAK,YAAY,MAAM,UAAU;EACjC,KAAK,aAAa,MAAM,eAAe;GACtC,WAAW,UAAU,CAAC,MAAM;GAE5B,UAAU,eAAe,iBAAiB,IAAI,GAAG,wBAAwB,gBAAgB;EAC1F,EAAE;CACH;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAU,SAAe;EACxB,MAAM,OAAO,KAAK;EAClB,MAAM,YAAY,KAAK,UAAU;EACjC,MAAM,YAAY,KAAK,SAAS,KAAK,QAAQ,KAAK,UAAU;EAC5D,MAAM,QAAQ,KAAK,SAAS,KAAK,KAAK;EAEtC,KAAK,MAAM,YAAY,uBAAuB,OAAO,KAAK,CAAC;EAC3D,KAAK,MAAM,YAAY,wBAAwB,OAAO,KAAK,eAAe,CAAC,CAAC;EAC5E,KAAK,MAAM,YAAY,iCAAiC,OAAO,KAAK,IAAI,OAAO,SAAS,CAAC,CAAC;EAC1F,KAAK,MAAM,YAAY,+BAA+B,OAAO,KAAK,IAAI,OAAO,SAAS,CAAC,CAAC;EACxF,IAAI,cAAc,QAAW,KAAK,MAAM,eAAe,yBAAyB;OAC3E,KAAK,MAAM,YAAY,2BAA2B,OAAO,SAAS,CAAC;EACxE,KAAK,WAAW,IAAI;CACrB;AACD"}
1
+ {"version":3,"file":"dial.js","names":[],"sources":["../../../src/elements/dial.ts"],"sourcesContent":["import { SonicRangeElement } from '#elements/range-element.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-dial': SonicDial;\n\t}\n}\n\nconst renderDial = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-dial\" role=\"slider\" tabindex=\"0\">\n\t\t\t<div class=\"sonic-dial-notches\"></div>\n\t\t\t<div class=\"sonic-dial-ring\"></div>\n\t\t\t<div class=\"sonic-dial-modulation\"></div>\n\t\t\t<div class=\"sonic-dial-cap\"></div>\n\t\t\t<div class=\"sonic-dial-pointer\"></div>\n\t\t\t<div class=\"sonic-dial-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-dial-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst fallbackTravelPx = 160;\n\nexport class SonicDial extends SonicRangeElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicRangeElement.observedAttributes,\n\t\t'endless',\n\t\t'modulation',\n\t];\n\n\tget endless(): boolean {\n\t\treturn this.hasAttribute('endless');\n\t}\n\n\tset endless(isEndless: boolean) {\n\t\tthis.reflect('endless', isEndless);\n\t}\n\n\tget modulation(): number {\n\t\treturn this.numberAttribute('modulation', 0);\n\t}\n\n\tset modulation(amount: number | undefined) {\n\t\tthis.reflect('modulation', amount);\n\t}\n\n\treadonly #dial = renderDial();\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('endless', 'modulation');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst dial = this.#dial;\n\n\t\tthis.appendOnce(dial);\n\t\tthis.render();\n\t\tthis.checkStyles(dial, 'dial.css');\n\t\tthis.bindGestures(dial, signal, () => ({\n\t\t\tposition: (event) => -event.clientY,\n\t\t\t// Once per gesture; a computed style read per move forces a style recalc\n\t\t\ttravelPx: readPxProperty(getComputedStyle(dial), '--_sonic-dial-travel', fallbackTravelPx),\n\t\t}));\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#dial;\n\t}\n\n\tprotected render(): void {\n\t\tconst dial = this.#dial;\n\t\tconst positions = this.positions();\n\t\tconst [modulationFrom, modulationTo] = this.modulationFractions(\n\t\t\tthis.endless ? 0 : this.modulation,\n\t\t);\n\n\t\tdial.style.setProperty('--_sonic-dial-value', String(this.fraction(this.value)));\n\t\tdial.style.setProperty('--_sonic-dial-origin', String(this.originFraction()));\n\t\tdial.style.setProperty('--_sonic-dial-modulation-from', String(modulationFrom));\n\t\tdial.style.setProperty('--_sonic-dial-modulation-to', String(modulationTo));\n\t\tif (positions === undefined) dial.style.removeProperty('--_sonic-dial-positions');\n\t\telse dial.style.setProperty('--_sonic-dial-positions', String(positions));\n\t\tthis.renderAria(dial);\n\t}\n\n\tprotected override wraps(): boolean {\n\t\treturn this.endless;\n\t}\n}\n"],"mappings":";;;;;AAUA,MAAM,aAAa,SACP;;;;;;;;;;;;;;;;;;;IAoBX,cACD;AAEA,MAAM,mBAAmB;AAEzB,IAAa,YAAb,cAA+B,kBAAkB;CAChD,OAAyB,qBAAqB;EAC7C,GAAG,kBAAkB;EACrB;EACA;CACD;CAEA,IAAI,UAAmB;EACtB,OAAO,KAAK,aAAa,SAAS;CACnC;CAEA,IAAI,QAAQ,WAAoB;EAC/B,KAAK,QAAQ,WAAW,SAAS;CAClC;CAEA,IAAI,aAAqB;EACxB,OAAO,KAAK,gBAAgB,cAAc,CAAC;CAC5C;CAEA,IAAI,WAAW,QAA4B;EAC1C,KAAK,QAAQ,cAAc,MAAM;CAClC;CAEA,AAAS,QAAQ,WAAW;CAE5B,AAAS,oBAA0B;EAClC,KAAK,kBAAkB,WAAW,YAAY;EAC9C,MAAM,kBAAkB;CACzB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,OAAO,KAAK;EAElB,KAAK,WAAW,IAAI;EACpB,KAAK,OAAO;EACZ,KAAK,YAAY,MAAM,UAAU;EACjC,KAAK,aAAa,MAAM,eAAe;GACtC,WAAW,UAAU,CAAC,MAAM;GAE5B,UAAU,eAAe,iBAAiB,IAAI,GAAG,wBAAwB,gBAAgB;EAC1F,EAAE;CACH;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAU,SAAe;EACxB,MAAM,OAAO,KAAK;EAClB,MAAM,YAAY,KAAK,UAAU;EACjC,MAAM,CAAC,gBAAgB,gBAAgB,KAAK,oBAC3C,KAAK,UAAU,IAAI,KAAK,UACzB;EAEA,KAAK,MAAM,YAAY,uBAAuB,OAAO,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC;EAC/E,KAAK,MAAM,YAAY,wBAAwB,OAAO,KAAK,eAAe,CAAC,CAAC;EAC5E,KAAK,MAAM,YAAY,iCAAiC,OAAO,cAAc,CAAC;EAC9E,KAAK,MAAM,YAAY,+BAA+B,OAAO,YAAY,CAAC;EAC1E,IAAI,cAAc,QAAW,KAAK,MAAM,eAAe,yBAAyB;OAC3E,KAAK,MAAM,YAAY,2BAA2B,OAAO,SAAS,CAAC;EACxE,KAAK,WAAW,IAAI;CACrB;CAEA,AAAmB,QAAiB;EACnC,OAAO,KAAK;CACb;AACD"}
@@ -0,0 +1,16 @@
1
+ import { SonicRangeElement } from "./range-element.js";
2
+ //#region src/elements/number.d.ts
3
+ declare global {
4
+ interface HTMLElementTagNameMap {
5
+ 'sonic-number': SonicNumber;
6
+ }
7
+ }
8
+ export declare class SonicNumber extends SonicRangeElement {
9
+ #private;
10
+ protected connect(signal: AbortSignal): void;
11
+ protected controlRole(): 'spinbutton';
12
+ protected focusTarget(): HTMLElement;
13
+ protected render(): void;
14
+ }
15
+ //#endregion
16
+ //# sourceMappingURL=number.d.ts.map
@@ -0,0 +1,47 @@
1
+ import { requireChild, template } from "../lib/render.js";
2
+ import { SonicRangeElement } from "./range-element.js";
3
+ import { readPxProperty } from "../lib/read-px-property.js";
4
+
5
+ //#region src/elements/number.ts
6
+ const renderNumber = template(`
7
+ <div class="sonic-number" role="spinbutton" tabindex="0">
8
+ <span aria-hidden="true" class="sonic-number-value"></span>
9
+ <input
10
+ autocomplete="off"
11
+ class="sonic-number-entry"
12
+ enterkeyhint="done"
13
+ hidden
14
+ spellcheck="false"
15
+ type="text"
16
+ />
17
+ </div>
18
+ `, HTMLDivElement);
19
+ const fallbackTravelPx = 160;
20
+ var SonicNumber = class extends SonicRangeElement {
21
+ #number = renderNumber();
22
+ #digits = requireChild(this.#number, ".sonic-number-value", HTMLSpanElement);
23
+ connect(signal) {
24
+ const number = this.#number;
25
+ this.appendOnce(number);
26
+ this.render();
27
+ this.checkStyles(number, "number.css");
28
+ this.bindGestures(number, signal, () => ({
29
+ position: (event) => -event.clientY,
30
+ travelPx: readPxProperty(getComputedStyle(number), "--_sonic-number-travel", fallbackTravelPx)
31
+ }));
32
+ }
33
+ controlRole() {
34
+ return "spinbutton";
35
+ }
36
+ focusTarget() {
37
+ return this.#number;
38
+ }
39
+ render() {
40
+ this.#digits.textContent = this.valueText();
41
+ this.renderAria(this.#number);
42
+ }
43
+ };
44
+
45
+ //#endregion
46
+ export { SonicNumber };
47
+ //# sourceMappingURL=number.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number.js","names":[],"sources":["../../../src/elements/number.ts"],"sourcesContent":["import { SonicRangeElement } from '#elements/range-element.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-number': SonicNumber;\n\t}\n}\n\nconst renderNumber = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-number\" role=\"spinbutton\" tabindex=\"0\">\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-number-value\"></span>\n\t\t\t<input\n\t\t\t\tautocomplete=\"off\"\n\t\t\t\tclass=\"sonic-number-entry\"\n\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\thidden\n\t\t\t\tspellcheck=\"false\"\n\t\t\t\ttype=\"text\"\n\t\t\t/>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst fallbackTravelPx = 160;\n\nexport class SonicNumber extends SonicRangeElement {\n\treadonly #number = renderNumber();\n\n\treadonly #digits = requireChild(this.#number, '.sonic-number-value', HTMLSpanElement);\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst number = this.#number;\n\n\t\tthis.appendOnce(number);\n\t\tthis.render();\n\t\tthis.checkStyles(number, 'number.css');\n\t\tthis.bindGestures(number, signal, () => ({\n\t\t\tposition: (event) => -event.clientY,\n\t\t\ttravelPx: readPxProperty(\n\t\t\t\tgetComputedStyle(number),\n\t\t\t\t'--_sonic-number-travel',\n\t\t\t\tfallbackTravelPx,\n\t\t\t),\n\t\t}));\n\t}\n\n\tprotected override controlRole(): 'spinbutton' {\n\t\treturn 'spinbutton';\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#number;\n\t}\n\n\tprotected render(): void {\n\t\tthis.#digits.textContent = this.valueText();\n\t\tthis.renderAria(this.#number);\n\t}\n}\n"],"mappings":";;;;;AAUA,MAAM,eAAe,SACT;;;;;;;;;;;;IAaX,cACD;AAEA,MAAM,mBAAmB;AAEzB,IAAa,cAAb,cAAiC,kBAAkB;CAClD,AAAS,UAAU,aAAa;CAEhC,AAAS,UAAU,aAAa,KAAK,SAAS,uBAAuB,eAAe;CAEpF,AAAU,QAAQ,QAA2B;EAC5C,MAAM,SAAS,KAAK;EAEpB,KAAK,WAAW,MAAM;EACtB,KAAK,OAAO;EACZ,KAAK,YAAY,QAAQ,YAAY;EACrC,KAAK,aAAa,QAAQ,eAAe;GACxC,WAAW,UAAU,CAAC,MAAM;GAC5B,UAAU,eACT,iBAAiB,MAAM,GACvB,0BACA,gBACD;EACD,EAAE;CACH;CAEA,AAAmB,cAA4B;EAC9C,OAAO;CACR;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAU,SAAe;EACxB,KAAK,QAAQ,cAAc,KAAK,UAAU;EAC1C,KAAK,WAAW,KAAK,OAAO;CAC7B;AACD"}
@@ -39,14 +39,18 @@ export declare abstract class SonicRangeElement extends SonicFormElement {
39
39
  connectedCallback(): void;
40
40
  protected bindGestures(control: HTMLElement, signal: AbortSignal, grab: (event: PointerEvent) => RangeAxis | undefined): void;
41
41
  protected clampRange(next: number): number;
42
+ protected controlRole(): 'slider' | 'spinbutton';
42
43
  protected fraction(value: number): number;
43
44
  protected input(next: number): boolean;
45
+ protected modulationFractions(amount: number): [number, number];
44
46
  protected originFraction(): number;
45
47
  protected positions(): number | undefined;
46
48
  protected range(): number;
47
49
  protected renderAria(control: HTMLElement, orientation?: 'horizontal' | 'vertical'): void;
48
50
  protected restoreState(state: string): void;
49
51
  protected valueAt(fraction: number): number;
52
+ protected valueText(): string;
53
+ protected wraps(): boolean;
50
54
  }
51
55
  //#endregion
52
56
  //# sourceMappingURL=range-element.d.ts.map