@xsynaptic/sonic-ui 0.10.3 → 0.12.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 (68) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +12 -0
  3. package/dist/default/define/toggle.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/button.js +2 -0
  6. package/dist/default/elements/position-group.js +163 -0
  7. package/dist/default/elements/radio-group.js +34 -12
  8. package/dist/default/elements/sonic-element.js +11 -2
  9. package/dist/default/elements/switch.js +34 -168
  10. package/dist/default/elements/toggle.js +189 -0
  11. package/dist/default/elements/value-element.js +9 -0
  12. package/dist/default/elements/waveform.js +38 -19
  13. package/dist/default/elements/wavestrip.js +6 -1
  14. package/dist/default/index.js +2 -1
  15. package/dist/default/lib/key-press.js +27 -0
  16. package/dist/default/lib/strip-scene.js +4 -3
  17. package/dist/default/lib/toggle-travel.js +19 -0
  18. package/dist/dev/define/toggle.d.ts +2 -0
  19. package/dist/dev/define/toggle.js +8 -0
  20. package/dist/dev/define/toggle.js.map +1 -0
  21. package/dist/dev/define.d.ts +3 -1
  22. package/dist/dev/define.js +1 -0
  23. package/dist/dev/elements/button.js +2 -0
  24. package/dist/dev/elements/button.js.map +1 -1
  25. package/dist/dev/elements/position-group.d.ts +38 -0
  26. package/dist/dev/elements/position-group.js +164 -0
  27. package/dist/dev/elements/position-group.js.map +1 -0
  28. package/dist/dev/elements/radio-group.d.ts +4 -0
  29. package/dist/dev/elements/radio-group.js +34 -12
  30. package/dist/dev/elements/radio-group.js.map +1 -1
  31. package/dist/dev/elements/sonic-element.d.ts +1 -0
  32. package/dist/dev/elements/sonic-element.js +11 -2
  33. package/dist/dev/elements/sonic-element.js.map +1 -1
  34. package/dist/dev/elements/switch.d.ts +4 -19
  35. package/dist/dev/elements/switch.js +34 -168
  36. package/dist/dev/elements/switch.js.map +1 -1
  37. package/dist/dev/elements/toggle.d.ts +21 -0
  38. package/dist/dev/elements/toggle.js +197 -0
  39. package/dist/dev/elements/toggle.js.map +1 -0
  40. package/dist/dev/elements/value-element.d.ts +3 -0
  41. package/dist/dev/elements/value-element.js +9 -0
  42. package/dist/dev/elements/value-element.js.map +1 -1
  43. package/dist/dev/elements/waveform.d.ts +1 -1
  44. package/dist/dev/elements/waveform.js +38 -19
  45. package/dist/dev/elements/waveform.js.map +1 -1
  46. package/dist/dev/elements/wavestrip.d.ts +3 -0
  47. package/dist/dev/elements/wavestrip.js +6 -1
  48. package/dist/dev/elements/wavestrip.js.map +1 -1
  49. package/dist/dev/index.d.ts +3 -2
  50. package/dist/dev/index.js +2 -1
  51. package/dist/dev/lib/key-press.js +28 -0
  52. package/dist/dev/lib/key-press.js.map +1 -0
  53. package/dist/dev/lib/strip-scene.js +4 -3
  54. package/dist/dev/lib/strip-scene.js.map +1 -1
  55. package/dist/dev/lib/toggle-travel.js +20 -0
  56. package/dist/dev/lib/toggle-travel.js.map +1 -0
  57. package/dist/styles/button.css +2 -2
  58. package/dist/styles/controls.css +1 -0
  59. package/dist/styles/led.css +16 -13
  60. package/dist/styles/material.css +36 -9
  61. package/dist/styles/meter.css +7 -6
  62. package/dist/styles/segmented.css +8 -4
  63. package/dist/styles/spectrum.css +2 -2
  64. package/dist/styles/switch.css +6 -2
  65. package/dist/styles/toggle.css +481 -0
  66. package/dist/styles/waveform.css +5 -2
  67. package/dist/styles/wavestrip.css +7 -0
  68. package/package.json +2 -1
@@ -1,9 +1,7 @@
1
- import { clamp } from "../lib/math.js";
2
- import { writeAttribute } from "../lib/write-attribute.js";
3
- import { bindHold } from "../lib/hold.js";
4
1
  import { requireChild, template } from "../lib/render.js";
5
2
  import { bindDrag } from "../lib/pointer-drag.js";
6
- import { SonicRadioGroupElement, isUnder, optionValue } from "./radio-group.js";
3
+ import { isUnder, optionValue } from "./radio-group.js";
4
+ import { SonicPositionGroupElement } from "./position-group.js";
7
5
 
8
6
  //#region src/elements/switch.ts
9
7
  const renderSwitch = template(`
@@ -18,166 +16,60 @@ const renderPosition = template(`
18
16
  </button>
19
17
  `, HTMLButtonElement);
20
18
  const renderBare = template(`<button class="sonic-switch-position" role="switch" type="button"></button>`, HTMLButtonElement);
21
- const pressCentreZone = .08;
22
- function pressedIndex(index, count, offset) {
23
- if (count === 2) return index === 0 ? 1 : 0;
24
- const side = Math.abs(offset) < pressCentreZone ? 1 : Math.sign(offset);
25
- const next = index + side;
26
- return next < 0 || next >= count ? index - side : next;
27
- }
28
- var SonicSwitch = class extends SonicRadioGroupElement {
29
- static observedAttributes = [
30
- ...SonicRadioGroupElement.observedAttributes,
31
- "checked",
32
- "orientation"
33
- ];
34
- get checked() {
35
- return this.#isDirty ? this.#checked : this.defaultChecked;
36
- }
37
- set checked(isChecked) {
38
- this.#isDirty = true;
39
- this.#checked = isChecked === true;
40
- this.render();
41
- }
42
- get defaultChecked() {
43
- return this.hasAttribute("checked");
44
- }
45
- set defaultChecked(isChecked) {
46
- this.reflect("checked", isChecked);
47
- }
48
- get orientation() {
49
- return this.getAttribute("orientation") === "horizontal" ? "horizontal" : "vertical";
50
- }
51
- set orientation(direction) {
52
- this.reflect("orientation", direction);
53
- }
54
- get value() {
55
- const value = super.value;
56
- return value === "" && this.#isBare() ? "on" : value;
57
- }
58
- set value(next) {
59
- super.value = next;
60
- }
19
+ const pressCenterZone = .08;
20
+ var SonicSwitch = class extends SonicPositionGroupElement {
21
+ bare = renderBare();
22
+ defaultOrientation = "vertical";
61
23
  group = renderSwitch();
62
- #bare = renderBare();
63
24
  #bat = requireChild(this.group, ".sonic-switch-bat", HTMLSpanElement);
64
- #checked = false;
65
- #holding;
66
25
  #isClickSwallowed = false;
67
- #isDirty = false;
68
- attributeChangedCallback(name) {
69
- if (name === "checked") this.#isDirty = false;
70
- if (this.isDisabled()) this.#holding?.release();
71
- super.attributeChangedCallback(name);
72
- }
73
- formResetCallback() {
74
- this.#isDirty = false;
75
- super.formResetCallback();
76
- }
77
- activate() {
78
- if (this.#isBare()) this.#bare.click();
79
- else super.activate();
80
- }
81
- chooseByClick(option) {
82
- super.chooseByClick(option);
83
- if (this.#isMomentary(option)) this.#springBack();
84
- }
85
- chooseByKey(option, event) {
86
- if (!this.#isMomentary(option)) {
87
- super.chooseByKey(option, event);
88
- return;
89
- }
90
- if (event.repeat) return;
91
- this.#hold(option, event.key);
92
- option.focus();
93
- }
94
26
  claimPress(option, event) {
95
27
  if (this.#isOnBat(event)) return true;
96
- if (!this.#isMomentary(option)) return false;
28
+ const target = this.pressTarget(option);
29
+ if (this.isOptionDisabled(target) || !this.isMomentary(target)) return false;
97
30
  this.group.setPointerCapture(event.pointerId);
98
- this.#hold(option, event.pointerId);
31
+ this.hold(target, event.pointerId);
99
32
  return true;
100
33
  }
101
34
  connect(signal) {
102
35
  const group = this.group;
103
- this.upgradeProperties("defaultChecked", "checked", "orientation");
104
36
  super.connect(signal);
105
37
  this.checkStyles(group, "switch.css");
106
- this.#bare.addEventListener("click", (event) => {
38
+ this.bare.addEventListener("click", (event) => {
107
39
  const isSwallowed = this.#isClickSwallowed && event instanceof PointerEvent && event.pointerType !== "";
108
40
  this.#isClickSwallowed = false;
109
- if (!isSwallowed) this.#setChecked(!this.checked);
41
+ if (!isSwallowed) this.setChecked(!this.checked);
110
42
  }, { signal });
111
- this.#bare.addEventListener("pointerdown", () => {
43
+ this.bare.addEventListener("pointerdown", () => {
112
44
  this.#isClickSwallowed = false;
113
45
  }, { signal });
114
46
  this.#bindBat(group, signal);
115
- this.#holding = bindHold(group, {
116
- canHold: () => !this.isDisabled(),
117
- leave: "focusout",
118
- onHold: () => {
119
- this.toggleState("held", true);
120
- },
121
- onRelease: () => {
122
- this.toggleState("held", false);
123
- this.#springBack();
124
- }
125
- }, signal);
126
- }
127
- focusTarget() {
128
- return this.#isBare() ? this.#bare : super.focusTarget();
129
- }
130
- isWrapping() {
131
- return false;
132
47
  }
133
48
  releaseTarget(option) {
134
- if (this.#isMomentary(option)) return void 0;
135
- const options = this.options();
136
- if (options.length === 2 && optionValue(option) === this.value) return options.find((other) => other !== option);
137
- return option;
49
+ const target = this.pressTarget(option);
50
+ return this.isMomentary(target) || this.isOptionDisabled(target) ? void 0 : target;
138
51
  }
139
52
  render() {
140
53
  if (!this.isBound()) return;
141
- const group = this.group;
54
+ super.render();
142
55
  const options = this.options();
143
56
  if (options.length > 0) {
144
- this.#bare.remove();
145
- writeAttribute(group, "role", "radiogroup");
146
- writeAttribute(group, "data-sonic-position-count", String(options.length));
147
- super.render();
148
- this.#renderThrow(this.#checkedIndex(options));
57
+ this.#renderThrow(this.checkedIndex(options));
149
58
  return;
150
59
  }
151
- const bare = this.#bare;
152
- const isChecked = this.checked;
153
- writeAttribute(group, "role", void 0);
154
- delete group.dataset.sonicPositionCount;
155
- if (bare.parentElement !== group) group.append(bare);
156
- writeAttribute(bare, "disabled", this.isDisabled() ? "" : void 0);
157
- writeAttribute(bare, "aria-checked", String(isChecked));
158
- this.forwardNaming(group, false);
159
- this.forwardNaming(bare, true);
160
60
  const onSide = this.orientation === "horizontal" ? 1 : -1;
161
- group.style.setProperty("--_sonic-switch-at", String(isChecked ? onSide : -onSide));
162
- this.writeFormValue(isChecked ? this.value : null, String(isChecked));
61
+ this.group.style.setProperty("--_sonic-switch-at", String(this.checked ? onSide : -onSide));
163
62
  }
164
63
  renderOption() {
165
64
  return renderPosition();
166
65
  }
167
- restoreState(state) {
168
- if (this.#isBare()) this.checked = state === "true";
169
- else super.restoreState(state);
170
- }
171
- #along(event) {
172
- return this.orientation === "horizontal" ? event.clientX : event.clientY;
173
- }
174
66
  #bindBat(group, signal) {
175
67
  bindDrag(group, {
176
68
  grab: (event) => {
177
- if (!(event.target === this.#bare || this.optionOf(event.target) !== void 0) || this.isDisabled() || !this.#isOnBat(event)) return;
69
+ if (!(event.target === this.bare || this.optionOf(event.target) !== void 0) || this.isDisabled() || !this.#isOnBat(event)) return;
178
70
  return {
179
71
  hasMoved: false,
180
- last: this.#along(event),
72
+ last: this.along(event),
181
73
  width: this.#bat.getBoundingClientRect().width
182
74
  };
183
75
  },
@@ -187,83 +79,57 @@ var SonicSwitch = class extends SonicRadioGroupElement {
187
79
  },
188
80
  move: (press, event) => {
189
81
  if (this.isDisabled()) return;
190
- const delta = this.#along(event) - press.last;
82
+ const delta = this.along(event) - press.last;
191
83
  if (Math.abs(delta) < press.width / 4) return;
192
84
  press.hasMoved = true;
193
- press.last = this.#along(event);
85
+ press.last = this.along(event);
194
86
  this.#throwBy(Math.sign(delta), event.pointerId);
195
87
  }
196
88
  }, signal);
197
89
  }
198
- #checkedIndex(options) {
199
- return options.findIndex((option) => optionValue(option) === this.value);
200
- }
201
90
  #hasSprungFrom(option) {
202
- if (!option || !this.#isMomentary(option)) return false;
203
- this.#holding?.release();
91
+ if (!option || !this.isMomentary(option)) return false;
92
+ this.releaseHold();
204
93
  return this.value !== optionValue(option);
205
94
  }
206
- #hold(option, by) {
207
- if (this.#holding?.hold(by) === true) this.select(option);
208
- }
209
- #isBare() {
210
- return this.options().length === 0;
211
- }
212
- #isMomentary(option) {
213
- const options = this.options();
214
- return (option === options[0] || option === options.at(-1)) && options.length > 1 && option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-momentary") === true;
215
- }
216
95
  #isOnBat(event) {
217
96
  return isUnder(this.#bat, event.clientX, event.clientY);
218
97
  }
219
98
  #pressBat(event) {
220
99
  const options = this.options();
221
100
  if (options.length === 0) {
222
- this.#setChecked(!this.checked);
101
+ this.setChecked(!this.checked);
223
102
  return;
224
103
  }
225
104
  const box = this.#bat.getBoundingClientRect();
226
105
  const [start, size] = this.orientation === "horizontal" ? [box.left, box.width] : [box.top, box.height];
227
- const offset = (this.#along(event) - start) / size - .5;
228
- const option = options[pressedIndex(this.#checkedIndex(options), options.length, offset)];
106
+ const offset = (this.along(event) - start) / size - .5;
107
+ const index = this.checkedIndex(options);
108
+ const side = Math.abs(offset) < pressCenterZone ? 1 : Math.sign(offset);
109
+ const toward = this.stepFrom(index, side);
110
+ const option = toward && toward !== options[index] ? toward : this.stepFrom(index, -side);
229
111
  if (!option) return;
230
112
  this.chooseByClick(option);
231
113
  this.#refocus(options);
232
114
  }
233
115
  #refocus(options) {
234
- if (this.group.matches(":focus-within")) options[this.#checkedIndex(options)]?.focus();
116
+ if (this.group.matches(":focus-within")) options[this.checkedIndex(options)]?.focus();
235
117
  }
236
118
  #renderThrow(index) {
237
119
  const count = this.options().length;
238
120
  const at = index < 0 || count < 2 ? 0 : 2 * index / (count - 1) - 1;
239
121
  this.group.style.setProperty("--_sonic-switch-at", String(at));
240
122
  }
241
- #setChecked(isChecked) {
242
- if (isChecked === this.checked) return;
243
- this.checked = isChecked;
244
- this.dispatchEvent(new Event("change", { bubbles: true }));
245
- }
246
- #springBack() {
247
- const options = this.options();
248
- const index = this.#checkedIndex(options);
249
- const held = options[index];
250
- if (!held || !this.#isMomentary(held)) return;
251
- const rest = options[index === 0 ? 1 : index - 1];
252
- if (!rest) return;
253
- const isFocused = this.group.contains(this.ownerDocument.activeElement);
254
- this.select(rest);
255
- if (isFocused) rest.focus();
256
- }
257
123
  #throwBy(direction, pointerId) {
258
124
  const options = this.options();
259
125
  if (options.length === 0) {
260
- this.#setChecked(direction === (this.orientation === "horizontal" ? 1 : -1));
126
+ this.setChecked(direction === (this.orientation === "horizontal" ? 1 : -1));
261
127
  return;
262
128
  }
263
- const index = this.#checkedIndex(options);
264
- const option = options[clamp(index + direction, 0, options.length - 1)];
129
+ const index = this.checkedIndex(options);
130
+ const option = this.stepFrom(index, direction);
265
131
  if (!option || option === options[index] || this.#hasSprungFrom(options[index])) return;
266
- if (this.#isMomentary(option)) this.#hold(option, pointerId);
132
+ if (this.isMomentary(option)) this.hold(option, pointerId);
267
133
  else this.select(option);
268
134
  this.#refocus(options);
269
135
  }
@@ -0,0 +1,189 @@
1
+ import { placeChildren, requireChild, template } from "../lib/render.js";
2
+ import { bindDrag } from "../lib/pointer-drag.js";
3
+ import { dragThresholdPx } from "../lib/drag-step.js";
4
+ import { isUnder, optionValue } from "./radio-group.js";
5
+ import { SonicPositionGroupElement } from "./position-group.js";
6
+ import { positionAt, travelFrom } from "../lib/toggle-travel.js";
7
+
8
+ //#region src/elements/toggle.ts
9
+ const renderToggle = template(`
10
+ <div class="sonic-toggle">
11
+ <span aria-hidden="true" class="sonic-toggle-well"></span>
12
+ <span aria-hidden="true" class="sonic-toggle-cap"></span>
13
+ </div>
14
+ `, HTMLDivElement);
15
+ const renderPosition = template(`
16
+ <button class="sonic-toggle-position" role="radio" type="button">
17
+ <span class="sonic-toggle-legend"></span>
18
+ </button>
19
+ `, HTMLButtonElement);
20
+ const renderBare = template(`<button class="sonic-toggle-position" role="switch" type="button"></button>`, HTMLButtonElement);
21
+ var SonicToggle = class extends SonicPositionGroupElement {
22
+ bare = renderBare();
23
+ defaultOrientation = "horizontal";
24
+ group = renderToggle();
25
+ #cap = requireChild(this.group, ".sonic-toggle-cap", HTMLSpanElement);
26
+ #draggedTo;
27
+ #hasWarnedOfLegends = false;
28
+ #well = requireChild(this.group, ".sonic-toggle-well", HTMLSpanElement);
29
+ claimPress() {
30
+ return true;
31
+ }
32
+ connect(signal) {
33
+ super.connect(signal);
34
+ this.checkStyles(this.group, "toggle.css");
35
+ this.bare.addEventListener("click", (event) => {
36
+ if (event instanceof PointerEvent && event.pointerType !== "") return;
37
+ this.setChecked(!this.checked);
38
+ }, { signal });
39
+ this.#bindCap(signal);
40
+ }
41
+ isMirrored(child) {
42
+ return super.isMirrored(child) || child === this.#fixedLegend();
43
+ }
44
+ placeCopies(copies) {
45
+ const positions = copies.filter((copy) => super.isMirrored(copy));
46
+ placeChildren(this.#cap, copies.filter((copy) => !positions.includes(copy)));
47
+ super.placeCopies(positions);
48
+ }
49
+ render() {
50
+ if (!this.isBound()) return;
51
+ this.group.style.setProperty("--_sonic-toggle-count", String(this.#stopCount()));
52
+ this.#placeCap(this.#draggedTo ?? this.#restingAt());
53
+ super.render();
54
+ for (const [index, option] of this.options().entries()) option.style.setProperty("--_sonic-toggle-index", String(index));
55
+ }
56
+ renderOption() {
57
+ return renderPosition();
58
+ }
59
+ #bindCap(signal) {
60
+ bindDrag(this.group, {
61
+ grab: (event) => this.#grab(event),
62
+ lift: (drag, event) => {
63
+ if (drag.hasMoved || drag.isHolding || this.isDisabled()) return;
64
+ if (!isUnder(this.group, event.clientX, event.clientY)) return;
65
+ this.#press(drag.position);
66
+ },
67
+ move: (drag, event) => {
68
+ if (drag.isHolding || this.isDisabled()) return;
69
+ const pointer = this.along(event);
70
+ if (!drag.hasMoved && Math.abs(pointer - drag.start) < drag.thresholdPx) return;
71
+ drag.hasMoved = true;
72
+ this.toggleState("dragging", true);
73
+ this.#slideTo(positionAt(drag.travel, pointer), event.pointerId);
74
+ },
75
+ release: (drag) => {
76
+ delete (drag.position ?? this.bare).dataset.sonicPressed;
77
+ this.#draggedTo = void 0;
78
+ this.toggleState("dragging", false);
79
+ this.render();
80
+ }
81
+ }, signal);
82
+ }
83
+ #checkLegends() {}
84
+ #enabledEnds() {
85
+ if (this.isBare()) return [0, 1];
86
+ const options = this.options();
87
+ const isEnabled = (option) => !this.isOptionDisabled(option);
88
+ return [options.findIndex(isEnabled), options.findLastIndex(isEnabled)];
89
+ }
90
+ #fixedLegend() {
91
+ return [...this.children].find((child) => child !== this.group && !super.isMirrored(child));
92
+ }
93
+ #grab(event) {
94
+ const position = this.optionOf(event.target);
95
+ if (this.isDisabled() || !position && event.target !== this.bare) return void 0;
96
+ const isOnCap = isUnder(this.#cap, event.clientX, event.clientY);
97
+ const travel = this.#travel(event, isOnCap);
98
+ if (!travel || this.#isBlocked(position, isOnCap)) return void 0;
99
+ (position ?? this.bare).toggleAttribute("data-sonic-pressed", true);
100
+ return {
101
+ hasMoved: false,
102
+ isHolding: this.#holdFrom(position, event.pointerId),
103
+ position,
104
+ start: this.along(event),
105
+ thresholdPx: dragThresholdPx(event.pointerType),
106
+ travel
107
+ };
108
+ }
109
+ #holdFrom(position, pointerId) {
110
+ const target = position && this.pressTarget(position);
111
+ if (!target || !this.isMomentary(target) || this.isOptionDisabled(target)) return false;
112
+ this.hold(target, pointerId);
113
+ return true;
114
+ }
115
+ #isBlocked(position, isOnCap) {
116
+ return !isOnCap && position !== void 0 && this.isOptionDisabled(position);
117
+ }
118
+ #onAt() {
119
+ return this.orientation === "vertical" ? 0 : 1;
120
+ }
121
+ #placeCap(at) {
122
+ this.group.style.setProperty("--_sonic-toggle-at", String(at));
123
+ }
124
+ #press(position) {
125
+ if (!position) {
126
+ this.setChecked(!this.checked);
127
+ return;
128
+ }
129
+ const target = this.pressTarget(position);
130
+ if (this.isOptionDisabled(target)) return;
131
+ this.select(target);
132
+ this.#refocus(target);
133
+ }
134
+ #refocus(position) {
135
+ if (this.group.matches(":focus-within")) position.focus();
136
+ }
137
+ #restingAt() {
138
+ if (this.isBare()) return this.checked ? this.#onAt() : 1 - this.#onAt();
139
+ return Math.max(0, this.checkedIndex(this.options()));
140
+ }
141
+ #slideTo(at, pointerId) {
142
+ const crossed = this.options()[Math.round(at)];
143
+ this.#draggedTo = at;
144
+ this.#placeCap(at);
145
+ if (this.isBare()) {
146
+ this.setChecked(Math.round(at) === this.#onAt());
147
+ return;
148
+ }
149
+ if (!crossed || optionValue(crossed) === this.value) return;
150
+ this.releaseHold();
151
+ if (this.isOptionDisabled(crossed) || optionValue(crossed) === this.value) return;
152
+ if (this.isMomentary(crossed)) this.hold(crossed, pointerId);
153
+ else this.select(crossed);
154
+ this.#refocus(crossed);
155
+ }
156
+ #stopCount() {
157
+ return this.options().length || 2;
158
+ }
159
+ #travel(event, isOnCap) {
160
+ const [first, last] = this.#enabledEnds();
161
+ if (first < 0) return void 0;
162
+ const box = this.#well.getBoundingClientRect();
163
+ const [start, length, thickness] = this.orientation === "vertical" ? [
164
+ box.top,
165
+ box.height,
166
+ box.width
167
+ ] : [
168
+ box.left,
169
+ box.width,
170
+ box.height
171
+ ];
172
+ return travelFrom({
173
+ at: this.#restingAt(),
174
+ count: this.#stopCount(),
175
+ first,
176
+ isOnCap,
177
+ last,
178
+ pointer: this.along(event),
179
+ well: {
180
+ length,
181
+ start,
182
+ thickness
183
+ }
184
+ });
185
+ }
186
+ };
187
+
188
+ //#endregion
189
+ export { SonicToggle };
@@ -72,6 +72,9 @@ var SonicValueElement = class extends SonicFormElement {
72
72
  }
73
73
  });
74
74
  }
75
+ get cancelling() {
76
+ return this.hasState("cancelling");
77
+ }
75
78
  get default() {
76
79
  return this.optionalNumberAttribute("default");
77
80
  }
@@ -97,6 +100,9 @@ var SonicValueElement = class extends SonicFormElement {
97
100
  set doublePress(gesture) {
98
101
  this.reflect("double-press", gesture);
99
102
  }
103
+ get dragging() {
104
+ return this.hasState("dragging");
105
+ }
100
106
  get formatEntry() {
101
107
  return this.#formatEntry;
102
108
  }
@@ -177,6 +183,9 @@ var SonicValueElement = class extends SonicFormElement {
177
183
  set readout(isShown) {
178
184
  this.reflect("readout", isShown);
179
185
  }
186
+ get revealed() {
187
+ return this.hasState("revealed");
188
+ }
180
189
  get spokenStep() {
181
190
  return this.optionalNumberAttribute("spoken-step");
182
191
  }
@@ -73,9 +73,9 @@ function paintPlaceholders(context, scene) {
73
73
  if (scene.pending.length === 0) return;
74
74
  const { dpr, height, pixelsPerSecond, startSeconds } = scene;
75
75
  const wavelength = placeholderWavelengthPx * dpr;
76
- const centre = height / 2;
76
+ const center = height / 2;
77
77
  const origin = startSeconds * pixelsPerSecond;
78
- const waveY = (x) => centre - Math.sin(((x + origin) / wavelength + scene.phase) * 2 * Math.PI) * centre * placeholderAmplitude;
78
+ const waveY = (x) => center - Math.sin(((x + origin) / wavelength + scene.phase) * 2 * Math.PI) * center * placeholderAmplitude;
79
79
  context.beginPath();
80
80
  for (const [from, to] of scene.pending) {
81
81
  const opening = (from - startSeconds) * pixelsPerSecond;
@@ -100,13 +100,13 @@ function paintWave(context, scene, peaks) {
100
100
  const buckets = waveformBuckets(peaks, scene);
101
101
  const [first] = buckets;
102
102
  if (!first) return;
103
- const centre = scene.height / 2;
104
- const amplitudePx = centre * amplitudeMargin;
103
+ const center = scene.height / 2;
104
+ const amplitudePx = center * amplitudeMargin;
105
105
  context.fillStyle = waveFill(context, scene);
106
106
  context.beginPath();
107
- context.moveTo(first.x, centre);
108
- for (const bucket of buckets) context.lineTo(bucket.x, centre - bucket.high * amplitudePx);
109
- for (const bucket of buckets.toReversed()) context.lineTo(bucket.x, centre - bucket.low * amplitudePx);
107
+ context.moveTo(first.x, center);
108
+ for (const bucket of buckets) context.lineTo(bucket.x, center - bucket.high * amplitudePx);
109
+ for (const bucket of buckets.toReversed()) context.lineTo(bucket.x, center - bucket.low * amplitudePx);
110
110
  context.closePath();
111
111
  context.fill();
112
112
  }
@@ -125,20 +125,22 @@ var SonicWaveform = class extends SonicWaveElement {
125
125
  set peaks(peaks) {
126
126
  this.#peaks = peaks;
127
127
  this.renderEmpty();
128
- this.surface()?.invalidate();
128
+ this.#repaint();
129
129
  }
130
130
  get pending() {
131
131
  return this.#pending.map(([start, end]) => [start, end]);
132
132
  }
133
133
  set pending(regions) {
134
+ const pending = regions ? readRegions(regions) : [];
135
+ if (String(pending) === String(this.#pending)) return;
134
136
  const listedMs = this.#pendingListedMs;
135
137
  const nowMs = performance.now();
136
- this.#pending = regions ? readRegions(regions) : [];
138
+ this.#pending = pending;
137
139
  this.#pendingListedMs = new Map(this.#pending.map((region) => {
138
140
  const key = region.join(":");
139
141
  return [key, listedMs.get(key) ?? nowMs];
140
142
  }));
141
- this.surface()?.invalidate();
143
+ this.#repaint();
142
144
  }
143
145
  get pendingDelay() {
144
146
  return Math.max(0, this.numberAttribute("pending-delay", 0));
@@ -177,7 +179,7 @@ var SonicWaveform = class extends SonicWaveElement {
177
179
  }
178
180
  set requestPeaks(request) {
179
181
  this.#requestPeaks = request;
180
- this.#asked = void 0;
182
+ this.#askedKey = void 0;
181
183
  this.surface()?.invalidate();
182
184
  }
183
185
  get zoom() {
@@ -193,11 +195,13 @@ var SonicWaveform = class extends SonicWaveElement {
193
195
  lengths = {};
194
196
  sheet = "waveform.css";
195
197
  sizeProperty = "--_sonic-waveform-size";
196
- #asked;
198
+ #asked = /* @__PURE__ */ new WeakSet();
199
+ #askedKey;
197
200
  #clock = createTrackingClock();
198
201
  #drawn;
199
202
  #ghost = requireChild(this.control, ".sonic-waveform-ghost", HTMLDivElement);
200
203
  #grabbed;
204
+ #isAsking = false;
201
205
  #kindColours = /* @__PURE__ */ new Map();
202
206
  #paintedKey = "";
203
207
  #peaks;
@@ -220,6 +224,7 @@ var SonicWaveform = class extends SonicWaveElement {
220
224
  connect(signal) {
221
225
  super.connect(signal);
222
226
  this.bindGestures(this.control, signal, () => this.#grab());
227
+ if (!("fonts" in document)) return;
223
228
  document.fonts.addEventListener("loadingdone", () => {
224
229
  this.renderMarkers();
225
230
  }, { signal });
@@ -287,10 +292,22 @@ var SonicWaveform = class extends SonicWaveElement {
287
292
  this.surface()?.rebuild();
288
293
  }
289
294
  #ask(wanted) {
290
- const asked = this.#requestPeaks?.(...wanted);
291
- if (!(asked instanceof Promise)) return;
292
- this.#asked = asked;
293
- this.#repaintAfter(asked);
295
+ const key = wanted.join(":");
296
+ if (key === this.#askedKey) return;
297
+ let asked;
298
+ this.#askedKey = key;
299
+ this.#isAsking = true;
300
+ try {
301
+ asked = this.#requestPeaks?.(...wanted);
302
+ } finally {
303
+ this.#isAsking = false;
304
+ }
305
+ const promises = asked instanceof Promise ? [asked] : asked ?? [];
306
+ for (const promise of promises) {
307
+ if (this.#asked.has(promise)) continue;
308
+ this.#asked.add(promise);
309
+ this.#repaintAfter(promise);
310
+ }
294
311
  }
295
312
  #duePending() {
296
313
  const due = dueRegions(this.#pending, this.#pendingListedMs, [performance.now(), this.pendingDelay]);
@@ -363,11 +380,13 @@ var SonicWaveform = class extends SonicWaveElement {
363
380
  for (const probe of probes) probe.remove();
364
381
  return new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
365
382
  }
383
+ #repaint() {
384
+ if (!this.#isAsking) this.#askedKey = void 0;
385
+ this.surface()?.invalidate();
386
+ }
366
387
  async #repaintAfter(asked) {
367
388
  await Promise.allSettled([asked]);
368
- if (this.#asked !== asked) return;
369
- this.#asked = void 0;
370
- this.surface()?.invalidate();
389
+ this.#repaint();
371
390
  }
372
391
  #sourceSeconds() {
373
392
  return this.#readTime?.() ?? this.playback();
@@ -16,6 +16,7 @@ const colours = {
16
16
  const lengths = {
17
17
  gapRatio: "--_sonic-wavestrip-bar-gap-ratio",
18
18
  grooveRatio: "--_sonic-wavestrip-groove-ratio",
19
+ grooveSize: "--_sonic-wavestrip-groove-size",
19
20
  pitch: "--_sonic-wavestrip-bar-pitch",
20
21
  radiusRatio: "--_sonic-wavestrip-bar-radius-ratio"
21
22
  };
@@ -79,6 +80,10 @@ var SonicWavestrip = class extends SonicWaveElement {
79
80
  #markerBand = createMarkerBand(requireChild(this.control, ".sonic-wavestrip-markers", HTMLDivElement));
80
81
  #paintedKey = "";
81
82
  #peaks;
83
+ clientXOf(value) {
84
+ const { startPx, travelPx } = this.#axis();
85
+ return startPx + this.mapping().proportionOf(value) * travelPx;
86
+ }
82
87
  connectedCallback() {
83
88
  this.upgradeProperties("buffered", "cancellable", "peaks");
84
89
  super.connectedCallback();
@@ -141,7 +146,7 @@ var SonicWavestrip = class extends SonicWaveElement {
141
146
  #barsFor({ isDirty, look, size }) {
142
147
  if (!isDirty && this.#bars) return this.#bars;
143
148
  const peaks = this.#peaks;
144
- const groove = stripGroove(size, look.lengths.grooveRatio);
149
+ const groove = stripGroove(size, look.lengths);
145
150
  const drawn = groove ? [groove] : [];
146
151
  const bars = peaks && peaks.length > 0 ? stripBars(peaks, size, look.lengths) : drawn;
147
152
  this.#bars = void 0;
@@ -9,10 +9,11 @@ import { SonicSlider } from "./elements/slider.js";
9
9
  import { SonicSpectrum } from "./elements/spectrum.js";
10
10
  import { SonicSplit } from "./elements/split.js";
11
11
  import { SonicSwitch } from "./elements/switch.js";
12
+ import { SonicToggle } from "./elements/toggle.js";
12
13
  import { SonicWaveform } from "./elements/waveform.js";
13
14
  import { SonicWavestrip } from "./elements/wavestrip.js";
14
15
  import { SonicXy } from "./elements/xy.js";
15
16
  import { crossfadeGains } from "./lib/crossfade.js";
16
17
  import { formatPercent, parsePercent } from "./lib/percent.js";
17
18
 
18
- export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
19
+ export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };