@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
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";
@@ -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,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,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;
@@ -0,0 +1,46 @@
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 };