@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
@@ -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
@@ -1,10 +1,11 @@
1
- import { template } from "../lib/render.js";
2
- import { SonicRangeElement } from "./range-element.js";
1
+ import { requireChild, template } from "../lib/render.js";
3
2
  import { readPxProperty } from "../lib/read-px-property.js";
3
+ import { SonicRangeElement } from "./range-element.js";
4
4
 
5
5
  //#region src/elements/dial.ts
6
6
  const renderDial = template(`
7
7
  <div class="sonic-dial" role="slider" tabindex="0">
8
+ <div class="sonic-dial-scale" aria-hidden="true"></div>
8
9
  <div class="sonic-dial-notches"></div>
9
10
  <div class="sonic-dial-ring"></div>
10
11
  <div class="sonic-dial-modulation"></div>
@@ -25,7 +26,17 @@ const renderDial = template(`
25
26
  `, HTMLDivElement);
26
27
  const fallbackTravelPx = 160;
27
28
  var SonicDial = class extends SonicRangeElement {
28
- static observedAttributes = [...SonicRangeElement.observedAttributes, "modulation"];
29
+ static observedAttributes = [
30
+ ...SonicRangeElement.observedAttributes,
31
+ "endless",
32
+ "modulation"
33
+ ];
34
+ get endless() {
35
+ return this.hasAttribute("endless");
36
+ }
37
+ set endless(isEndless) {
38
+ this.reflect("endless", isEndless);
39
+ }
29
40
  get modulation() {
30
41
  return this.numberAttribute("modulation", 0);
31
42
  }
@@ -34,12 +45,12 @@ var SonicDial = class extends SonicRangeElement {
34
45
  }
35
46
  #dial = renderDial();
36
47
  connectedCallback() {
37
- this.upgradeProperties("modulation");
48
+ this.upgradeProperties("endless", "modulation");
38
49
  super.connectedCallback();
39
50
  }
40
51
  connect(signal) {
41
52
  const dial = this.#dial;
42
- this.appendOnce(dial);
53
+ this.bindScale(dial, requireChild(dial, ".sonic-dial-scale", HTMLDivElement), signal);
43
54
  this.render();
44
55
  this.checkStyles(dial, "dial.css");
45
56
  this.bindGestures(dial, signal, () => ({
@@ -53,16 +64,19 @@ var SonicDial = class extends SonicRangeElement {
53
64
  render() {
54
65
  const dial = this.#dial;
55
66
  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));
67
+ const [modulationFrom, modulationTo] = this.modulationFractions(this.endless ? 0 : this.modulation);
68
+ dial.style.setProperty("--_sonic-dial-value", String(this.fraction(this.value)));
59
69
  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)));
70
+ dial.style.setProperty("--_sonic-dial-modulation-from", String(modulationFrom));
71
+ dial.style.setProperty("--_sonic-dial-modulation-to", String(modulationTo));
62
72
  if (positions === void 0) dial.style.removeProperty("--_sonic-dial-positions");
63
73
  else dial.style.setProperty("--_sonic-dial-positions", String(positions));
74
+ this.renderScale();
64
75
  this.renderAria(dial);
65
76
  }
77
+ wraps() {
78
+ return this.endless;
79
+ }
66
80
  };
67
81
 
68
82
  //#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 { requireChild, 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-scale\" aria-hidden=\"true\"></div>\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\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute, as on a native element\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.bindScale(dial, requireChild(dial, '.sonic-dial-scale', HTMLDivElement), signal);\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.renderScale();\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;;;;;;;;;;;;;;;;;;;;IAqBX,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;CAGA,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,UAAU,MAAM,aAAa,MAAM,qBAAqB,cAAc,GAAG,MAAM;EACpF,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,YAAY;EACjB,KAAK,WAAW,IAAI;CACrB;CAEA,AAAmB,QAAiB;EACnC,OAAO,KAAK;CACb;AACD"}
@@ -8,12 +8,16 @@ declare global {
8
8
  export declare class SonicKey extends SonicFormElement {
9
9
  #private;
10
10
  static readonly observedAttributes: string[];
11
+ get busy(): boolean;
12
+ set busy(isBusy: boolean);
11
13
  get defaultPressed(): boolean;
12
14
  set defaultPressed(isPressed: boolean);
13
15
  get momentary(): boolean;
14
16
  set momentary(isMomentary: boolean);
15
17
  get pressed(): boolean;
16
18
  set pressed(isPressed: boolean | undefined);
19
+ get softDisabled(): boolean;
20
+ set softDisabled(isSoftDisabled: boolean);
17
21
  get toggle(): boolean;
18
22
  set toggle(isToggle: boolean);
19
23
  get value(): string;
@@ -1,4 +1,5 @@
1
1
  import { requireChild, template } from "../lib/render.js";
2
+ import { writeAttribute } from "../lib/write-attribute.js";
2
3
  import { SonicFormElement } from "./form-element.js";
3
4
  import { copyNode } from "../lib/copy-node.js";
4
5
 
@@ -10,11 +11,19 @@ const holdKeys = /* @__PURE__ */ new Set([" ", "Enter"]);
10
11
  var SonicKey = class extends SonicFormElement {
11
12
  static observedAttributes = [
12
13
  ...SonicFormElement.observedAttributes,
14
+ "busy",
13
15
  "momentary",
14
16
  "pressed",
17
+ "soft-disabled",
15
18
  "toggle",
16
19
  "value"
17
20
  ];
21
+ get busy() {
22
+ return this.hasAttribute("busy");
23
+ }
24
+ set busy(isBusy) {
25
+ this.reflect("busy", isBusy);
26
+ }
18
27
  get defaultPressed() {
19
28
  return this.hasAttribute("pressed");
20
29
  }
@@ -35,6 +44,12 @@ var SonicKey = class extends SonicFormElement {
35
44
  this.#pressed = isPressed === true;
36
45
  this.render();
37
46
  }
47
+ get softDisabled() {
48
+ return this.hasAttribute("soft-disabled");
49
+ }
50
+ set softDisabled(isSoftDisabled) {
51
+ this.reflect("soft-disabled", isSoftDisabled);
52
+ }
38
53
  get toggle() {
39
54
  return this.hasAttribute("toggle");
40
55
  }
@@ -53,7 +68,7 @@ var SonicKey = class extends SonicFormElement {
53
68
  #pressed = false;
54
69
  attributeChangedCallback(name) {
55
70
  if (name === "pressed") this.#isDirty = false;
56
- if (name === "disabled" && this.isDisabled()) this.#release();
71
+ if (this.isDisabled() || this.#isSoftDisabled()) this.#release();
57
72
  this.render();
58
73
  }
59
74
  formResetCallback() {
@@ -65,7 +80,7 @@ var SonicKey = class extends SonicFormElement {
65
80
  connect(signal) {
66
81
  const key = this.#key;
67
82
  const cap = requireChild(key, ".sonic-key-cap", HTMLSpanElement);
68
- this.upgradeProperties("defaultPressed", "momentary", "toggle", "pressed", "value");
83
+ this.upgradeProperties("busy", "defaultPressed", "momentary", "softDisabled", "toggle", "pressed", "value");
69
84
  this.mirrorChildren({
70
85
  control: key,
71
86
  copy: (originals) => {
@@ -77,7 +92,12 @@ var SonicKey = class extends SonicFormElement {
77
92
  signal.addEventListener("abort", () => {
78
93
  this.#release();
79
94
  }, { once: true });
80
- key.addEventListener("click", () => {
95
+ key.addEventListener("click", (event) => {
96
+ if (this.#isSoftDisabled()) {
97
+ event.preventDefault();
98
+ event.stopImmediatePropagation();
99
+ return;
100
+ }
81
101
  if (!this.toggle) return;
82
102
  this.pressed = !this.pressed;
83
103
  this.dispatchEvent(new Event("change", { bubbles: true }));
@@ -90,6 +110,8 @@ var SonicKey = class extends SonicFormElement {
90
110
  render() {
91
111
  const key = this.#key;
92
112
  key.disabled = this.isDisabled();
113
+ writeAttribute(key, "aria-disabled", this.#isSoftDisabled() ? "true" : void 0);
114
+ writeAttribute(key, "aria-busy", this.busy ? "true" : void 0);
93
115
  key.removeAttribute("aria-pressed");
94
116
  if (this.toggle) key.setAttribute("aria-pressed", String(this.pressed));
95
117
  this.toggleState("pressed", this.pressed);
@@ -124,7 +146,7 @@ var SonicKey = class extends SonicFormElement {
124
146
  }, { signal });
125
147
  }
126
148
  #hold(holder) {
127
- if (this.isDisabled() || this.#holder !== void 0) return;
149
+ if (this.isDisabled() || this.#isSoftDisabled() || this.#holder !== void 0) return;
128
150
  this.#holder = holder;
129
151
  this.pressed = true;
130
152
  this.dispatchEvent(new Event("change", { bubbles: true }));
@@ -132,6 +154,9 @@ var SonicKey = class extends SonicFormElement {
132
154
  #isMomentary() {
133
155
  return this.momentary && !this.toggle;
134
156
  }
157
+ #isSoftDisabled() {
158
+ return this.softDisabled && !this.isDisabled();
159
+ }
135
160
  #release() {
136
161
  if (this.#holder === void 0) return;
137
162
  this.#holder = void 0;
@@ -1 +1 @@
1
- {"version":3,"file":"key.js","names":[],"sources":["../../../src/elements/key.ts"],"sourcesContent":["import { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-key': SonicKey;\n\t}\n}\n\nconst renderKey = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-key\" type=\"button\"><span class=\"sonic-key-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nexport class SonicKey extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t'momentary',\n\t\t'pressed',\n\t\t'toggle',\n\t\t'value',\n\t];\n\n\t// The `pressed` attribute, as `defaultChecked` holds a checkbox's `checked` attribute\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\t// The live state, which never writes the attribute, as `value` on the range\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget toggle(): boolean {\n\t\treturn this.hasAttribute('toggle');\n\t}\n\n\tset toggle(isToggle: boolean) {\n\t\tthis.reflect('toggle', isToggle);\n\t}\n\n\t// What a pressed toggle submits, reading `on` while unset as a checkbox does\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\t#holder: number | string | undefined;\n\n\t// Re-read from the attribute until a press or a write\n\t// An attribute write clears it, so a framework driving `pressed` keeps control\n\t#isDirty = false;\n\n\treadonly #key = renderKey();\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (name === 'disabled' && this.isDisabled()) this.#release();\n\t\tthis.render();\n\t}\n\n\t// Back to the `pressed` attribute, as a checkbox resets to `checked`\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\t// As a checkbox's label toggles it\n\tprotected override activate(): void {\n\t\tthis.#key.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst key = this.#key;\n\t\tconst cap = requireChild(key, '.sonic-key-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties('defaultPressed', 'momentary', 'toggle', 'pressed', 'value');\n\t\tthis.mirrorChildren(\n\t\t\t{\n\t\t\t\tcontrol: key,\n\t\t\t\tcopy: (originals) => {\n\t\t\t\t\tcap.replaceChildren(...originals.map((original) => copyNode(original)));\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(key, 'key.css');\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tthis.#release();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\n\t\tkey.addEventListener(\n\t\t\t'click',\n\t\t\t() => {\n\t\t\t\tif (!this.toggle) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(key, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#key;\n\t}\n\n\tprotected render(): void {\n\t\tconst key = this.#key;\n\n\t\tkey.disabled = this.isDisabled();\n\t\tkey.removeAttribute('aria-pressed');\n\t\tif (this.toggle) key.setAttribute('aria-pressed', String(this.pressed));\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(key, true);\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.toggle && this.pressed ? this.value : null;\n\n\t\t// The state restores an unpressed toggle too\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.toggle) this.pressed = state === 'true';\n\t}\n\n\t// Not `click`, which lands a tap late by the finger's dwell\n\t#bindMomentary(key: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst releasePointer = (event: PointerEvent): void => {\n\t\t\tif (event.pointerId === this.#holder) this.#release();\n\t\t};\n\n\t\tkey.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tkey.setPointerCapture(event.pointerId);\n\t\t\t\tthis.#hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tkey.addEventListener('lostpointercapture', releasePointer, { signal });\n\t\tkey.addEventListener('pointercancel', releasePointer, { signal });\n\t\tkey.addEventListener('pointerup', releasePointer, { signal });\n\t\tkey.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tthis.#hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tkey.addEventListener(\n\t\t\t'keyup',\n\t\t\t(event) => {\n\t\t\t\tif (event.key === this.#holder) this.#release();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// Keyboard holds only; a finger on the next pad takes focus while this one is still held\n\t\tkey.addEventListener(\n\t\t\t'blur',\n\t\t\t() => {\n\t\t\t\tif (typeof this.#holder === 'string') this.#release();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#hold(holder: number | string): void {\n\t\tif (this.isDisabled() || this.#holder !== undefined) return;\n\n\t\tthis.#holder = holder;\n\t\tthis.pressed = true;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.toggle;\n\t}\n\n\t#release(): void {\n\t\tif (this.#holder === undefined) return;\n\n\t\tthis.#holder = undefined;\n\t\tthis.pressed = false;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"key.js","names":[],"sources":["../../../src/elements/key.ts"],"sourcesContent":["import { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-key': SonicKey;\n\t}\n}\n\nconst renderKey = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-key\" type=\"button\"><span class=\"sonic-key-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nexport class SonicKey extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t'busy',\n\t\t'momentary',\n\t\t'pressed',\n\t\t'soft-disabled',\n\t\t'toggle',\n\t\t'value',\n\t];\n\n\tget busy(): boolean {\n\t\treturn this.hasAttribute('busy');\n\t}\n\n\tset busy(isBusy: boolean) {\n\t\tthis.reflect('busy', isBusy);\n\t}\n\n\t// The `pressed` attribute, as `defaultChecked` holds a checkbox's `checked` attribute\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\t// The live state, which never writes the attribute, as `value` on the range\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget softDisabled(): boolean {\n\t\treturn this.hasAttribute('soft-disabled');\n\t}\n\n\tset softDisabled(isSoftDisabled: boolean) {\n\t\tthis.reflect('soft-disabled', isSoftDisabled);\n\t}\n\n\tget toggle(): boolean {\n\t\treturn this.hasAttribute('toggle');\n\t}\n\n\tset toggle(isToggle: boolean) {\n\t\tthis.reflect('toggle', isToggle);\n\t}\n\n\t// What a pressed toggle submits, reading `on` while unset as a checkbox does\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\t#holder: number | string | undefined;\n\n\t// Re-read from the attribute until a press or a write\n\t// An attribute write clears it, so a framework driving `pressed` keeps control\n\t#isDirty = false;\n\n\treadonly #key = renderKey();\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (this.isDisabled() || this.#isSoftDisabled()) this.#release();\n\t\tthis.render();\n\t}\n\n\t// Back to the `pressed` attribute, as a checkbox resets to `checked`\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\t// As a checkbox's label toggles it\n\tprotected override activate(): void {\n\t\tthis.#key.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst key = this.#key;\n\t\tconst cap = requireChild(key, '.sonic-key-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties(\n\t\t\t'busy',\n\t\t\t'defaultPressed',\n\t\t\t'momentary',\n\t\t\t'softDisabled',\n\t\t\t'toggle',\n\t\t\t'pressed',\n\t\t\t'value',\n\t\t);\n\t\tthis.mirrorChildren(\n\t\t\t{\n\t\t\t\tcontrol: key,\n\t\t\t\tcopy: (originals) => {\n\t\t\t\t\tcap.replaceChildren(...originals.map((original) => copyNode(original)));\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(key, 'key.css');\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tthis.#release();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\n\t\tkey.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A native disabled button fires no click at all\n\t\t\t\tif (this.#isSoftDisabled()) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.toggle) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(key, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#key;\n\t}\n\n\tprotected render(): void {\n\t\tconst key = this.#key;\n\n\t\tkey.disabled = this.isDisabled();\n\t\twriteAttribute(key, 'aria-disabled', this.#isSoftDisabled() ? 'true' : undefined);\n\t\twriteAttribute(key, 'aria-busy', this.busy ? 'true' : undefined);\n\t\tkey.removeAttribute('aria-pressed');\n\t\tif (this.toggle) key.setAttribute('aria-pressed', String(this.pressed));\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(key, true);\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.toggle && this.pressed ? this.value : null;\n\n\t\t// The state restores an unpressed toggle too\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.toggle) this.pressed = state === 'true';\n\t}\n\n\t// Not `click`, which lands a tap late by the finger's dwell\n\t#bindMomentary(key: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst releasePointer = (event: PointerEvent): void => {\n\t\t\tif (event.pointerId === this.#holder) this.#release();\n\t\t};\n\n\t\tkey.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tkey.setPointerCapture(event.pointerId);\n\t\t\t\tthis.#hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tkey.addEventListener('lostpointercapture', releasePointer, { signal });\n\t\tkey.addEventListener('pointercancel', releasePointer, { signal });\n\t\tkey.addEventListener('pointerup', releasePointer, { signal });\n\t\tkey.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tthis.#hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tkey.addEventListener(\n\t\t\t'keyup',\n\t\t\t(event) => {\n\t\t\t\tif (event.key === this.#holder) this.#release();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// Keyboard holds only; a finger on the next pad takes focus while this one is still held\n\t\tkey.addEventListener(\n\t\t\t'blur',\n\t\t\t() => {\n\t\t\t\tif (typeof this.#holder === 'string') this.#release();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#hold(holder: number | string): void {\n\t\tif (this.isDisabled() || this.#isSoftDisabled() || this.#holder !== undefined) return;\n\n\t\tthis.#holder = holder;\n\t\tthis.pressed = true;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.toggle;\n\t}\n\n\t#isSoftDisabled(): boolean {\n\t\treturn this.softDisabled && !this.isDisabled();\n\t}\n\n\t#release(): void {\n\t\tif (this.#holder === undefined) return;\n\n\t\tthis.#holder = undefined;\n\t\tthis.pressed = false;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n}\n"],"mappings":"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"}
@@ -10,12 +10,18 @@ export declare class SonicMeter extends SonicElement {
10
10
  static readonly observedAttributes: string[];
11
11
  get level(): number;
12
12
  set level(amplitude: number);
13
+ get lights(): Array<number> | undefined;
14
+ set lights(thresholds: Array<number> | undefined);
13
15
  get max(): number;
14
16
  set max(decibels: number | undefined);
15
17
  get min(): number;
16
18
  set min(decibels: number | undefined);
17
19
  get orientation(): 'horizontal' | 'vertical';
18
20
  set orientation(direction: 'horizontal' | 'vertical' | undefined);
21
+ get origin(): number | undefined;
22
+ set origin(value: number | undefined);
23
+ get scale(): 'db' | 'linear';
24
+ set scale(scale: 'db' | 'linear' | undefined);
19
25
  attributeChangedCallback(): void;
20
26
  protected connect(signal: AbortSignal): void;
21
27
  }
@@ -1,5 +1,7 @@
1
1
  import { template } from "../lib/render.js";
2
2
  import { SonicElement } from "./sonic-element.js";
3
+ import { parseNumberList } from "../lib/number-list.js";
4
+ import { linearTaper } from "../lib/taper.js";
3
5
 
4
6
  //#region src/elements/meter.ts
5
7
  const renderMeter = template(`
@@ -14,20 +16,30 @@ const holdMs = 1500;
14
16
  function toDecibels(amplitude) {
15
17
  return 20 * Math.log10(Math.abs(amplitude));
16
18
  }
19
+ function lightsBelow(lights, zone) {
20
+ return `calc(${lights.map((threshold) => `sign(max(0, var(--_sonic-meter-${zone}-from) - (${String(threshold)})))`).join(" + ")})`;
21
+ }
17
22
  var SonicMeter = class extends SonicElement {
18
23
  static observedAttributes = [
19
24
  "disabled",
25
+ "lights",
20
26
  "max",
21
- "min"
27
+ "min",
28
+ "origin",
29
+ "scale"
22
30
  ];
23
31
  get level() {
24
32
  return this.#level;
25
33
  }
26
34
  set level(amplitude) {
27
35
  if (!Number.isFinite(amplitude)) return;
28
- const decibels = toDecibels(amplitude);
29
36
  const now = performance.now();
30
37
  this.#level = amplitude;
38
+ if (this.scale === "linear") {
39
+ this.#render(now);
40
+ return;
41
+ }
42
+ const decibels = toDecibels(amplitude);
31
43
  this.#target = decibels;
32
44
  this.#bar = Math.max(this.#bar, decibels);
33
45
  if (decibels >= this.#peak) {
@@ -38,6 +50,12 @@ var SonicMeter = class extends SonicElement {
38
50
  this.#render(now);
39
51
  this.#schedule(now);
40
52
  }
53
+ get lights() {
54
+ return parseNumberList(this.getAttribute("lights"));
55
+ }
56
+ set lights(thresholds) {
57
+ this.reflect("lights", thresholds?.join(" "));
58
+ }
41
59
  get max() {
42
60
  return this.numberAttribute("max", 0);
43
61
  }
@@ -56,24 +74,39 @@ var SonicMeter = class extends SonicElement {
56
74
  set orientation(direction) {
57
75
  this.reflect("orientation", direction);
58
76
  }
77
+ get origin() {
78
+ return this.optionalNumberAttribute("origin");
79
+ }
80
+ set origin(value) {
81
+ this.reflect("origin", value);
82
+ }
83
+ get scale() {
84
+ return this.getAttribute("scale") === "linear" ? "linear" : "db";
85
+ }
86
+ set scale(scale) {
87
+ this.reflect("scale", scale);
88
+ }
59
89
  #bar = -Infinity;
60
90
  #clipAt = -Infinity;
61
91
  #frame;
62
92
  #lastFrame;
63
93
  #level = 0;
94
+ #lights;
64
95
  #meter = renderMeter();
65
96
  #peak = -Infinity;
66
97
  #peakAt = -Infinity;
67
98
  #target = -Infinity;
68
99
  attributeChangedCallback() {
69
100
  const now = performance.now();
101
+ this.#renderLights();
70
102
  this.#render(now);
71
103
  this.#schedule(now);
72
104
  }
73
105
  connect(signal) {
74
106
  const now = performance.now();
75
- this.upgradeProperties("max", "min", "orientation", "level");
107
+ this.upgradeProperties("lights", "max", "min", "orientation", "origin", "scale", "level");
76
108
  this.appendOnce(this.#meter);
109
+ this.#renderLights();
77
110
  this.#render(now);
78
111
  this.#schedule(now);
79
112
  this.checkStyles(this.#meter, "meter.css");
@@ -83,21 +116,25 @@ var SonicMeter = class extends SonicElement {
83
116
  this.#lastFrame = void 0;
84
117
  }, { once: true });
85
118
  }
86
- #fraction(decibels) {
87
- const min = this.min;
88
- const range = this.max - min;
89
- return range > 0 ? Math.min(1, Math.max(0, (decibels - min) / range)) : 0;
119
+ #fraction(level) {
120
+ const lights = this.#lights;
121
+ if (lights) return lights.filter((threshold) => threshold <= level).length / lights.length;
122
+ return linearTaper(this.min, this.max).position(level);
90
123
  }
91
124
  #isSettled(now) {
125
+ if (this.scale === "linear") return true;
92
126
  const floor = this.min;
93
127
  return this.#bar <= Math.max(this.#target, floor) && this.#peak <= Math.max(this.#bar, floor) && now - this.#clipAt >= holdMs;
94
128
  }
95
129
  #render(now) {
96
130
  const meter = this.#meter;
131
+ const origin = this.origin === void 0 ? 0 : this.#fraction(this.origin);
97
132
  meter.style.setProperty("--_sonic-meter-min", String(this.min));
98
133
  meter.style.setProperty("--_sonic-meter-max", String(this.max));
99
- if (this.disabled) {
100
- meter.style.setProperty("--_sonic-meter-level", "0");
134
+ meter.style.setProperty("--_sonic-meter-origin", String(origin));
135
+ if (this.disabled || this.scale === "linear") {
136
+ const level = this.disabled ? origin : this.#fraction(this.#level);
137
+ meter.style.setProperty("--_sonic-meter-level", String(level));
101
138
  meter.style.setProperty("--_sonic-meter-peak", "0");
102
139
  meter.style.setProperty("--_sonic-meter-clipped", "0");
103
140
  return;
@@ -106,6 +143,23 @@ var SonicMeter = class extends SonicElement {
106
143
  meter.style.setProperty("--_sonic-meter-peak", String(this.#fraction(this.#peak)));
107
144
  meter.style.setProperty("--_sonic-meter-clipped", now - this.#clipAt < holdMs ? "1" : "0");
108
145
  }
146
+ #renderLights() {
147
+ const meter = this.#meter;
148
+ const lights = this.lights;
149
+ this.#lights = lights;
150
+ this.toggleState("ladder", lights !== void 0);
151
+ if (!lights) {
152
+ for (const name of [
153
+ "count",
154
+ "hot-lights",
155
+ "clip-lights"
156
+ ]) meter.style.removeProperty(`--_sonic-meter-${name}`);
157
+ return;
158
+ }
159
+ meter.style.setProperty("--_sonic-meter-count", String(lights.length));
160
+ meter.style.setProperty("--_sonic-meter-hot-lights", lightsBelow(lights, "hot"));
161
+ meter.style.setProperty("--_sonic-meter-clip-lights", lightsBelow(lights, "clip"));
162
+ }
109
163
  #schedule(now) {
110
164
  if (this.#frame !== void 0 || !this.isConnected || this.#isSettled(now)) return;
111
165
  if (this.#lastFrame === void 0) this.#lastFrame = now;
@@ -1 +1 @@
1
- {"version":3,"file":"meter.js","names":[],"sources":["../../../src/elements/meter.ts"],"sourcesContent":["import { SonicElement } from '#elements/sonic-element.ts';\nimport { template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-meter': SonicMeter;\n\t}\n}\n\nconst renderMeter = template(\n\t/* HTML */ `\n\t\t<div aria-hidden=\"true\" class=\"sonic-meter\">\n\t\t\t<div class=\"sonic-meter-segments\"></div>\n\t\t\t<div class=\"sonic-meter-level\"></div>\n\t\t\t<div class=\"sonic-meter-clip\"></div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst releaseDecibelsPerSecond = 20;\nconst holdMs = 1500;\n\nfunction toDecibels(amplitude: number): number {\n\treturn 20 * Math.log10(Math.abs(amplitude));\n}\n\n// `level` is a linear amplitude; `min` and `max` are dBFS\nexport class SonicMeter extends SonicElement {\n\tstatic override readonly observedAttributes = ['disabled', 'max', 'min'];\n\n\tget level(): number {\n\t\treturn this.#level;\n\t}\n\n\tset level(amplitude: number) {\n\t\tif (!Number.isFinite(amplitude)) return;\n\n\t\tconst decibels = toDecibels(amplitude);\n\t\tconst now = performance.now();\n\n\t\tthis.#level = amplitude;\n\t\tthis.#target = decibels;\n\t\tthis.#bar = Math.max(this.#bar, decibels);\n\t\tif (decibels >= this.#peak) {\n\t\t\tthis.#peak = decibels;\n\t\t\tthis.#peakAt = now;\n\t\t}\n\t\tif (decibels >= 0) this.#clipAt = now;\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t}\n\n\tget max(): number {\n\t\treturn this.numberAttribute('max', 0);\n\t}\n\n\tset max(decibels: number | undefined) {\n\t\tthis.reflect('max', decibels);\n\t}\n\n\tget min(): number {\n\t\treturn this.numberAttribute('min', -60);\n\t}\n\n\tset min(decibels: number | undefined) {\n\t\tthis.reflect('min', decibels);\n\t}\n\n\tget orientation(): 'horizontal' | 'vertical' {\n\t\treturn this.getAttribute('orientation') === 'horizontal' ? 'horizontal' : 'vertical';\n\t}\n\n\tset orientation(direction: 'horizontal' | 'vertical' | undefined) {\n\t\tthis.reflect('orientation', direction);\n\t}\n\n\t#bar = -Infinity;\n\n\t#clipAt = -Infinity;\n\n\t#frame: number | undefined;\n\n\t#lastFrame: number | undefined;\n\n\t#level = 0;\n\n\treadonly #meter = renderMeter();\n\n\t#peak = -Infinity;\n\n\t#peakAt = -Infinity;\n\n\t#target = -Infinity;\n\n\tattributeChangedCallback(): void {\n\t\tconst now = performance.now();\n\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst now = performance.now();\n\n\t\tthis.upgradeProperties('max', 'min', 'orientation', 'level');\n\t\tthis.appendOnce(this.#meter);\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t\tthis.checkStyles(this.#meter, 'meter.css');\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t\tthis.#lastFrame = undefined;\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#fraction(decibels: number): number {\n\t\tconst min = this.min;\n\t\tconst range = this.max - min;\n\n\t\treturn range > 0 ? Math.min(1, Math.max(0, (decibels - min) / range)) : 0;\n\t}\n\n\t#isSettled(now: number): boolean {\n\t\tconst floor = this.min;\n\n\t\treturn (\n\t\t\tthis.#bar <= Math.max(this.#target, floor) &&\n\t\t\tthis.#peak <= Math.max(this.#bar, floor) &&\n\t\t\tnow - this.#clipAt >= holdMs\n\t\t);\n\t}\n\n\t#render(now: number): void {\n\t\tconst meter = this.#meter;\n\n\t\tmeter.style.setProperty('--_sonic-meter-min', String(this.min));\n\t\tmeter.style.setProperty('--_sonic-meter-max', String(this.max));\n\n\t\t// Disabled puts the light out, as on the other controls; the ballistics keep running so it lights up mid-fall\n\t\tif (this.disabled) {\n\t\t\tmeter.style.setProperty('--_sonic-meter-level', '0');\n\t\t\tmeter.style.setProperty('--_sonic-meter-peak', '0');\n\t\t\tmeter.style.setProperty('--_sonic-meter-clipped', '0');\n\t\t\treturn;\n\t\t}\n\n\t\tmeter.style.setProperty('--_sonic-meter-level', String(this.#fraction(this.#bar)));\n\t\tmeter.style.setProperty('--_sonic-meter-peak', String(this.#fraction(this.#peak)));\n\t\tmeter.style.setProperty('--_sonic-meter-clipped', now - this.#clipAt < holdMs ? '1' : '0');\n\t}\n\n\t#schedule(now: number): void {\n\t\tif (this.#frame !== undefined || !this.isConnected || this.#isSettled(now)) return;\n\n\t\tif (this.#lastFrame === undefined) this.#lastFrame = now;\n\t\tthis.#frame = requestAnimationFrame(this.#tick);\n\t}\n\n\t// Timed by the frame rather than counted, so a 120Hz display falls at the same speed\n\treadonly #tick = (time: number): void => {\n\t\tconst fall = (releaseDecibelsPerSecond * Math.max(0, time - (this.#lastFrame ?? time))) / 1000;\n\n\t\tthis.#frame = undefined;\n\t\tthis.#lastFrame = time;\n\t\tthis.#bar = Math.max(this.#target, this.min, this.#bar - fall);\n\t\tif (time - this.#peakAt >= holdMs) this.#peak = Math.max(this.#bar, this.#peak - fall);\n\t\tthis.#render(time);\n\t\tif (this.#isSettled(time)) {\n\t\t\tthis.#lastFrame = undefined;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#frame = requestAnimationFrame(this.#tick);\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"meter.js","names":[],"sources":["../../../src/elements/meter.ts"],"sourcesContent":["import { SonicElement } from '#elements/sonic-element.ts';\nimport { parseNumberList } from '#lib/number-list.ts';\nimport { template } from '#lib/render.ts';\nimport { linearTaper } from '#lib/taper.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-meter': SonicMeter;\n\t}\n}\n\nconst renderMeter = template(\n\t/* HTML */ `\n\t\t<div aria-hidden=\"true\" class=\"sonic-meter\">\n\t\t\t<div class=\"sonic-meter-segments\"></div>\n\t\t\t<div class=\"sonic-meter-level\"></div>\n\t\t\t<div class=\"sonic-meter-clip\"></div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst releaseDecibelsPerSecond = 20;\nconst holdMs = 1500;\n\nfunction toDecibels(amplitude: number): number {\n\treturn 20 * Math.log10(Math.abs(amplitude));\n}\n\n// A `calc()` rather than a count, so the zone's token stays live\nfunction lightsBelow(lights: Array<number>, zone: 'clip' | 'hot'): string {\n\tconst terms = lights.map(\n\t\t(threshold) => `sign(max(0, var(--_sonic-meter-${zone}-from) - (${String(threshold)})))`,\n\t);\n\n\treturn `calc(${terms.join(' + ')})`;\n}\n\n// On the default `db` scale, `level` is a linear amplitude shown in dBFS; on `linear`, it is in the units of `min` and `max`\nexport class SonicMeter extends SonicElement {\n\tstatic override readonly observedAttributes = [\n\t\t'disabled',\n\t\t'lights',\n\t\t'max',\n\t\t'min',\n\t\t'origin',\n\t\t'scale',\n\t];\n\n\tget level(): number {\n\t\treturn this.#level;\n\t}\n\n\tset level(amplitude: number) {\n\t\tif (!Number.isFinite(amplitude)) return;\n\n\t\tconst now = performance.now();\n\n\t\tthis.#level = amplitude;\n\t\tif (this.scale === 'linear') {\n\t\t\tthis.#render(now);\n\t\t\treturn;\n\t\t}\n\n\t\tconst decibels = toDecibels(amplitude);\n\n\t\tthis.#target = decibels;\n\t\tthis.#bar = Math.max(this.#bar, decibels);\n\t\tif (decibels >= this.#peak) {\n\t\t\tthis.#peak = decibels;\n\t\t\tthis.#peakAt = now;\n\t\t}\n\t\tif (decibels >= 0) this.#clipAt = now;\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t}\n\n\tget lights(): Array<number> | undefined {\n\t\treturn parseNumberList(this.getAttribute('lights'));\n\t}\n\n\tset lights(thresholds: Array<number> | undefined) {\n\t\tthis.reflect('lights', thresholds?.join(' '));\n\t}\n\n\tget max(): number {\n\t\treturn this.numberAttribute('max', 0);\n\t}\n\n\tset max(decibels: number | undefined) {\n\t\tthis.reflect('max', decibels);\n\t}\n\n\tget min(): number {\n\t\treturn this.numberAttribute('min', -60);\n\t}\n\n\tset min(decibels: number | undefined) {\n\t\tthis.reflect('min', decibels);\n\t}\n\n\tget orientation(): 'horizontal' | 'vertical' {\n\t\treturn this.getAttribute('orientation') === 'horizontal' ? 'horizontal' : 'vertical';\n\t}\n\n\tset orientation(direction: 'horizontal' | 'vertical' | undefined) {\n\t\tthis.reflect('orientation', direction);\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute, as on a native element\n\tget origin(): number | undefined {\n\t\treturn this.optionalNumberAttribute('origin');\n\t}\n\n\tset origin(value: number | undefined) {\n\t\tthis.reflect('origin', value);\n\t}\n\n\tget scale(): 'db' | 'linear' {\n\t\treturn this.getAttribute('scale') === 'linear' ? 'linear' : 'db';\n\t}\n\n\tset scale(scale: 'db' | 'linear' | undefined) {\n\t\tthis.reflect('scale', scale);\n\t}\n\n\t#bar = -Infinity;\n\n\t#clipAt = -Infinity;\n\n\t#frame: number | undefined;\n\n\t#lastFrame: number | undefined;\n\n\t#level = 0;\n\n\t// Parsed once per attribute change rather than per frame\n\t#lights: Array<number> | undefined;\n\n\treadonly #meter = renderMeter();\n\n\t#peak = -Infinity;\n\n\t#peakAt = -Infinity;\n\n\t#target = -Infinity;\n\n\tattributeChangedCallback(): void {\n\t\tconst now = performance.now();\n\n\t\tthis.#renderLights();\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst now = performance.now();\n\n\t\tthis.upgradeProperties('lights', 'max', 'min', 'orientation', 'origin', 'scale', 'level');\n\t\tthis.appendOnce(this.#meter);\n\t\tthis.#renderLights();\n\t\tthis.#render(now);\n\t\tthis.#schedule(now);\n\t\tthis.checkStyles(this.#meter, 'meter.css');\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t\tthis.#lastFrame = undefined;\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\t#fraction(level: number): number {\n\t\tconst lights = this.#lights;\n\t\tif (lights) return lights.filter((threshold) => threshold <= level).length / lights.length;\n\n\t\treturn linearTaper(this.min, this.max).position(level);\n\t}\n\n\t#isSettled(now: number): boolean {\n\t\tif (this.scale === 'linear') return true;\n\n\t\tconst floor = this.min;\n\n\t\treturn (\n\t\t\tthis.#bar <= Math.max(this.#target, floor) &&\n\t\t\tthis.#peak <= Math.max(this.#bar, floor) &&\n\t\t\tnow - this.#clipAt >= holdMs\n\t\t);\n\t}\n\n\t#render(now: number): void {\n\t\tconst meter = this.#meter;\n\n\t\tconst origin = this.origin === undefined ? 0 : this.#fraction(this.origin);\n\n\t\tmeter.style.setProperty('--_sonic-meter-min', String(this.min));\n\t\tmeter.style.setProperty('--_sonic-meter-max', String(this.max));\n\t\tmeter.style.setProperty('--_sonic-meter-origin', String(origin));\n\n\t\t// Disabled puts the light out, as on the other controls; the ballistics keep running so it lights up mid-fall\n\t\t// The consumer smooths a linear level, so it has no fall, hold or clip\n\t\tif (this.disabled || this.scale === 'linear') {\n\t\t\tconst level = this.disabled ? origin : this.#fraction(this.#level);\n\n\t\t\tmeter.style.setProperty('--_sonic-meter-level', String(level));\n\t\t\tmeter.style.setProperty('--_sonic-meter-peak', '0');\n\t\t\tmeter.style.setProperty('--_sonic-meter-clipped', '0');\n\t\t\treturn;\n\t\t}\n\n\t\tmeter.style.setProperty('--_sonic-meter-level', String(this.#fraction(this.#bar)));\n\t\tmeter.style.setProperty('--_sonic-meter-peak', String(this.#fraction(this.#peak)));\n\t\tmeter.style.setProperty('--_sonic-meter-clipped', now - this.#clipAt < holdMs ? '1' : '0');\n\t}\n\n\t#renderLights(): void {\n\t\tconst meter = this.#meter;\n\t\tconst lights = this.lights;\n\n\t\tthis.#lights = lights;\n\t\tthis.toggleState('ladder', lights !== undefined);\n\t\tif (!lights) {\n\t\t\tfor (const name of ['count', 'hot-lights', 'clip-lights']) {\n\t\t\t\tmeter.style.removeProperty(`--_sonic-meter-${name}`);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tmeter.style.setProperty('--_sonic-meter-count', String(lights.length));\n\t\tmeter.style.setProperty('--_sonic-meter-hot-lights', lightsBelow(lights, 'hot'));\n\t\tmeter.style.setProperty('--_sonic-meter-clip-lights', lightsBelow(lights, 'clip'));\n\t}\n\n\t#schedule(now: number): void {\n\t\tif (this.#frame !== undefined || !this.isConnected || this.#isSettled(now)) return;\n\n\t\tif (this.#lastFrame === undefined) this.#lastFrame = now;\n\t\tthis.#frame = requestAnimationFrame(this.#tick);\n\t}\n\n\t// Timed by the frame rather than counted, so a 120Hz display falls at the same speed\n\treadonly #tick = (time: number): void => {\n\t\tconst fall = (releaseDecibelsPerSecond * Math.max(0, time - (this.#lastFrame ?? time))) / 1000;\n\n\t\tthis.#frame = undefined;\n\t\tthis.#lastFrame = time;\n\t\tthis.#bar = Math.max(this.#target, this.min, this.#bar - fall);\n\t\tif (time - this.#peakAt >= holdMs) this.#peak = Math.max(this.#bar, this.#peak - fall);\n\t\tthis.#render(time);\n\t\tif (this.#isSettled(time)) {\n\t\t\tthis.#lastFrame = undefined;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#frame = requestAnimationFrame(this.#tick);\n\t};\n}\n"],"mappings":"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"}
@@ -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 { readPxProperty } from "../lib/read-px-property.js";
3
+ import { SonicRangeElement } from "./range-element.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"}
@@ -9,6 +9,8 @@ export declare abstract class SonicRangeElement extends SonicFormElement {
9
9
  static readonly observedAttributes: string[];
10
10
  get default(): number | undefined;
11
11
  set default(value: number | undefined);
12
+ get detent(): number | undefined;
13
+ set detent(value: number | undefined);
12
14
  get dimmed(): boolean;
13
15
  set dimmed(isDimmed: boolean);
14
16
  get doublePress(): 'reset' | 'type';
@@ -35,18 +37,28 @@ export declare abstract class SonicRangeElement extends SonicFormElement {
35
37
  set taper(curve: 'linear' | 'log' | undefined);
36
38
  get value(): number;
37
39
  set value(next: number);
40
+ get values(): Array<number> | undefined;
41
+ set values(entries: Array<number> | undefined);
38
42
  attributeChangedCallback(name: string): void;
39
43
  connectedCallback(): void;
40
44
  protected bindGestures(control: HTMLElement, signal: AbortSignal, grab: (event: PointerEvent) => RangeAxis | undefined): void;
45
+ protected bindScale(control: HTMLElement, scale: HTMLElement, signal: AbortSignal): void;
41
46
  protected clampRange(next: number): number;
47
+ protected controlRole(): 'slider' | 'spinbutton';
42
48
  protected fraction(value: number): number;
43
49
  protected input(next: number): boolean;
50
+ protected modulationFractions(amount: number): [number, number];
44
51
  protected originFraction(): number;
45
52
  protected positions(): number | undefined;
46
53
  protected range(): number;
47
54
  protected renderAria(control: HTMLElement, orientation?: 'horizontal' | 'vertical'): void;
55
+ protected renderScale(): void;
48
56
  protected restoreState(state: string): void;
57
+ protected restValue(): number;
58
+ protected springTarget(): number | undefined;
49
59
  protected valueAt(fraction: number): number;
60
+ protected valueText(): string;
61
+ protected wraps(): boolean;
50
62
  }
51
63
  //#endregion
52
64
  //# sourceMappingURL=range-element.d.ts.map