@xsynaptic/sonic-ui 0.10.4 → 0.13.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 (119) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +39 -15
  3. package/dist/default/define/toggle.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/button.js +7 -1
  6. package/dist/default/elements/dial.js +7 -1
  7. package/dist/default/elements/display-element.js +5 -1
  8. package/dist/default/elements/envelope.js +6 -1
  9. package/dist/default/elements/meter.js +1 -1
  10. package/dist/default/elements/number.js +7 -1
  11. package/dist/default/elements/position-group.js +163 -0
  12. package/dist/default/elements/radio-group.js +34 -12
  13. package/dist/default/elements/segmented.js +5 -1
  14. package/dist/default/elements/slider.js +7 -1
  15. package/dist/default/elements/sonic-element.js +1 -1
  16. package/dist/default/elements/switch.js +34 -168
  17. package/dist/default/elements/toggle.js +193 -0
  18. package/dist/default/elements/value-element.js +3 -0
  19. package/dist/default/elements/value-gestures.js +7 -0
  20. package/dist/default/elements/wave-element.js +8 -1
  21. package/dist/default/elements/waveform.js +159 -13
  22. package/dist/default/elements/wavestrip.js +4 -0
  23. package/dist/default/elements/xy.js +7 -1
  24. package/dist/default/index.js +2 -1
  25. package/dist/default/lib/key-press.js +27 -0
  26. package/dist/default/lib/toggle-travel.js +19 -0
  27. package/dist/default/lib/zoom-gesture.js +19 -0
  28. package/dist/dev/define/toggle.d.ts +2 -0
  29. package/dist/dev/define/toggle.js +8 -0
  30. package/dist/dev/define/toggle.js.map +1 -0
  31. package/dist/dev/define.d.ts +3 -1
  32. package/dist/dev/define.js +1 -0
  33. package/dist/dev/elements/button.js +7 -1
  34. package/dist/dev/elements/button.js.map +1 -1
  35. package/dist/dev/elements/dial.js +7 -1
  36. package/dist/dev/elements/dial.js.map +1 -1
  37. package/dist/dev/elements/display-element.js +5 -1
  38. package/dist/dev/elements/display-element.js.map +1 -1
  39. package/dist/dev/elements/envelope.js +6 -1
  40. package/dist/dev/elements/envelope.js.map +1 -1
  41. package/dist/dev/elements/meter.js +1 -1
  42. package/dist/dev/elements/meter.js.map +1 -1
  43. package/dist/dev/elements/number.js +7 -1
  44. package/dist/dev/elements/number.js.map +1 -1
  45. package/dist/dev/elements/position-group.d.ts +38 -0
  46. package/dist/dev/elements/position-group.js +164 -0
  47. package/dist/dev/elements/position-group.js.map +1 -0
  48. package/dist/dev/elements/radio-group.d.ts +4 -0
  49. package/dist/dev/elements/radio-group.js +34 -12
  50. package/dist/dev/elements/radio-group.js.map +1 -1
  51. package/dist/dev/elements/segmented.js +5 -1
  52. package/dist/dev/elements/segmented.js.map +1 -1
  53. package/dist/dev/elements/slider.js +7 -1
  54. package/dist/dev/elements/slider.js.map +1 -1
  55. package/dist/dev/elements/sonic-element.d.ts +16 -1
  56. package/dist/dev/elements/sonic-element.js +27 -5
  57. package/dist/dev/elements/sonic-element.js.map +1 -1
  58. package/dist/dev/elements/switch.d.ts +4 -19
  59. package/dist/dev/elements/switch.js +34 -168
  60. package/dist/dev/elements/switch.js.map +1 -1
  61. package/dist/dev/elements/toggle.d.ts +21 -0
  62. package/dist/dev/elements/toggle.js +201 -0
  63. package/dist/dev/elements/toggle.js.map +1 -0
  64. package/dist/dev/elements/value-element.d.ts +1 -0
  65. package/dist/dev/elements/value-element.js +3 -0
  66. package/dist/dev/elements/value-element.js.map +1 -1
  67. package/dist/dev/elements/value-gestures.js +7 -0
  68. package/dist/dev/elements/value-gestures.js.map +1 -1
  69. package/dist/dev/elements/wave-element.js +8 -1
  70. package/dist/dev/elements/wave-element.js.map +1 -1
  71. package/dist/dev/elements/waveform.d.ts +11 -1
  72. package/dist/dev/elements/waveform.js +159 -13
  73. package/dist/dev/elements/waveform.js.map +1 -1
  74. package/dist/dev/elements/wavestrip.d.ts +1 -0
  75. package/dist/dev/elements/wavestrip.js +4 -0
  76. package/dist/dev/elements/wavestrip.js.map +1 -1
  77. package/dist/dev/elements/xy.js +7 -1
  78. package/dist/dev/elements/xy.js.map +1 -1
  79. package/dist/dev/index.d.ts +3 -2
  80. package/dist/dev/index.js +2 -1
  81. package/dist/dev/lib/key-press.js +28 -0
  82. package/dist/dev/lib/key-press.js.map +1 -0
  83. package/dist/dev/lib/toggle-travel.js +20 -0
  84. package/dist/dev/lib/toggle-travel.js.map +1 -0
  85. package/dist/dev/lib/zoom-gesture.js +20 -0
  86. package/dist/dev/lib/zoom-gesture.js.map +1 -0
  87. package/dist/styles/button.css +5 -3
  88. package/dist/styles/controls.css +1 -0
  89. package/dist/styles/dial.css +3 -1
  90. package/dist/styles/envelope.css +3 -1
  91. package/dist/styles/led.css +19 -14
  92. package/dist/styles/material/arc.css +21 -0
  93. package/dist/styles/material/bracket.css +31 -0
  94. package/dist/styles/material/cap.css +179 -0
  95. package/dist/styles/material/core.css +103 -0
  96. package/dist/styles/material/glass.css +62 -0
  97. package/dist/styles/material/groove.css +10 -0
  98. package/dist/styles/material/keycap.css +95 -0
  99. package/dist/styles/material/lens.css +11 -0
  100. package/dist/styles/material/pane.css +97 -0
  101. package/dist/styles/material/readout.css +87 -0
  102. package/dist/styles/material/scale.css +30 -0
  103. package/dist/styles/material/value.css +43 -0
  104. package/dist/styles/material/well.css +24 -0
  105. package/dist/styles/material.css +15 -745
  106. package/dist/styles/meter.css +10 -7
  107. package/dist/styles/number.css +3 -1
  108. package/dist/styles/panel.css +3 -1
  109. package/dist/styles/ring.css +3 -1
  110. package/dist/styles/screen.css +3 -1
  111. package/dist/styles/segmented.css +11 -5
  112. package/dist/styles/slider.css +3 -1
  113. package/dist/styles/spectrum.css +5 -3
  114. package/dist/styles/switch.css +9 -3
  115. package/dist/styles/toggle.css +483 -0
  116. package/dist/styles/waveform.css +3 -1
  117. package/dist/styles/wavestrip.css +3 -1
  118. package/dist/styles/xy.css +3 -1
  119. package/package.json +3 -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(`
@@ -19,165 +17,59 @@ const renderPosition = template(`
19
17
  `, HTMLButtonElement);
20
18
  const renderBare = template(`<button class="sonic-switch-position" role="switch" type="button"></button>`, HTMLButtonElement);
21
19
  const pressCenterZone = .08;
22
- function pressedIndex(index, count, offset) {
23
- if (count === 2) return index === 0 ? 1 : 0;
24
- const side = Math.abs(offset) < pressCenterZone ? 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
- }
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
- this.checkStyles(group, "switch.css");
106
- this.#bare.addEventListener("click", (event) => {
37
+ this.checkStyles(group, "switch.css", ["cap"]);
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,193 @@
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
+ "cap",
36
+ "well",
37
+ "keycap"
38
+ ]);
39
+ this.bare.addEventListener("click", (event) => {
40
+ if (event instanceof PointerEvent && event.pointerType !== "") return;
41
+ this.setChecked(!this.checked);
42
+ }, { signal });
43
+ this.#bindCap(signal);
44
+ }
45
+ isMirrored(child) {
46
+ return super.isMirrored(child) || child === this.#fixedLegend();
47
+ }
48
+ placeCopies(copies) {
49
+ const positions = copies.filter((copy) => super.isMirrored(copy));
50
+ placeChildren(this.#cap, copies.filter((copy) => !positions.includes(copy)));
51
+ super.placeCopies(positions);
52
+ }
53
+ render() {
54
+ if (!this.isBound()) return;
55
+ this.group.style.setProperty("--_sonic-toggle-count", String(this.#stopCount()));
56
+ this.#placeCap(this.#draggedTo ?? this.#restingAt());
57
+ super.render();
58
+ for (const [index, option] of this.options().entries()) option.style.setProperty("--_sonic-toggle-index", String(index));
59
+ }
60
+ renderOption() {
61
+ return renderPosition();
62
+ }
63
+ #bindCap(signal) {
64
+ bindDrag(this.group, {
65
+ grab: (event) => this.#grab(event),
66
+ lift: (drag, event) => {
67
+ if (drag.hasMoved || drag.isHolding || this.isDisabled()) return;
68
+ if (!isUnder(this.group, event.clientX, event.clientY)) return;
69
+ this.#press(drag.position);
70
+ },
71
+ move: (drag, event) => {
72
+ if (drag.isHolding || this.isDisabled()) return;
73
+ const pointer = this.along(event);
74
+ if (!drag.hasMoved && Math.abs(pointer - drag.start) < drag.thresholdPx) return;
75
+ drag.hasMoved = true;
76
+ this.toggleState("dragging", true);
77
+ this.#slideTo(positionAt(drag.travel, pointer), event.pointerId);
78
+ },
79
+ release: (drag) => {
80
+ delete (drag.position ?? this.bare).dataset.sonicPressed;
81
+ this.#draggedTo = void 0;
82
+ this.toggleState("dragging", false);
83
+ this.render();
84
+ }
85
+ }, signal);
86
+ }
87
+ #checkLegends() {}
88
+ #enabledEnds() {
89
+ if (this.isBare()) return [0, 1];
90
+ const options = this.options();
91
+ const isEnabled = (option) => !this.isOptionDisabled(option);
92
+ return [options.findIndex(isEnabled), options.findLastIndex(isEnabled)];
93
+ }
94
+ #fixedLegend() {
95
+ return [...this.children].find((child) => child !== this.group && !super.isMirrored(child));
96
+ }
97
+ #grab(event) {
98
+ const position = this.optionOf(event.target);
99
+ if (this.isDisabled() || !position && event.target !== this.bare) return void 0;
100
+ const isOnCap = isUnder(this.#cap, event.clientX, event.clientY);
101
+ const travel = this.#travel(event, isOnCap);
102
+ if (!travel || this.#isBlocked(position, isOnCap)) return void 0;
103
+ (position ?? this.bare).toggleAttribute("data-sonic-pressed", true);
104
+ return {
105
+ hasMoved: false,
106
+ isHolding: this.#holdFrom(position, event.pointerId),
107
+ position,
108
+ start: this.along(event),
109
+ thresholdPx: dragThresholdPx(event.pointerType),
110
+ travel
111
+ };
112
+ }
113
+ #holdFrom(position, pointerId) {
114
+ const target = position && this.pressTarget(position);
115
+ if (!target || !this.isMomentary(target) || this.isOptionDisabled(target)) return false;
116
+ this.hold(target, pointerId);
117
+ return true;
118
+ }
119
+ #isBlocked(position, isOnCap) {
120
+ return !isOnCap && position !== void 0 && this.isOptionDisabled(position);
121
+ }
122
+ #onAt() {
123
+ return this.orientation === "vertical" ? 0 : 1;
124
+ }
125
+ #placeCap(at) {
126
+ this.group.style.setProperty("--_sonic-toggle-at", String(at));
127
+ }
128
+ #press(position) {
129
+ if (!position) {
130
+ this.setChecked(!this.checked);
131
+ return;
132
+ }
133
+ const target = this.pressTarget(position);
134
+ if (this.isOptionDisabled(target)) return;
135
+ this.select(target);
136
+ this.#refocus(target);
137
+ }
138
+ #refocus(position) {
139
+ if (this.group.matches(":focus-within")) position.focus();
140
+ }
141
+ #restingAt() {
142
+ if (this.isBare()) return this.checked ? this.#onAt() : 1 - this.#onAt();
143
+ return Math.max(0, this.checkedIndex(this.options()));
144
+ }
145
+ #slideTo(at, pointerId) {
146
+ const crossed = this.options()[Math.round(at)];
147
+ this.#draggedTo = at;
148
+ this.#placeCap(at);
149
+ if (this.isBare()) {
150
+ this.setChecked(Math.round(at) === this.#onAt());
151
+ return;
152
+ }
153
+ if (!crossed || optionValue(crossed) === this.value) return;
154
+ this.releaseHold();
155
+ if (this.isOptionDisabled(crossed) || optionValue(crossed) === this.value) return;
156
+ if (this.isMomentary(crossed)) this.hold(crossed, pointerId);
157
+ else this.select(crossed);
158
+ this.#refocus(crossed);
159
+ }
160
+ #stopCount() {
161
+ return this.options().length || 2;
162
+ }
163
+ #travel(event, isOnCap) {
164
+ const [first, last] = this.#enabledEnds();
165
+ if (first < 0) return void 0;
166
+ const box = this.#well.getBoundingClientRect();
167
+ const [start, length, thickness] = this.orientation === "vertical" ? [
168
+ box.top,
169
+ box.height,
170
+ box.width
171
+ ] : [
172
+ box.left,
173
+ box.width,
174
+ box.height
175
+ ];
176
+ return travelFrom({
177
+ at: this.#restingAt(),
178
+ count: this.#stopCount(),
179
+ first,
180
+ isOnCap,
181
+ last,
182
+ pointer: this.along(event),
183
+ well: {
184
+ length,
185
+ start,
186
+ thickness
187
+ }
188
+ });
189
+ }
190
+ };
191
+
192
+ //#endregion
193
+ export { SonicToggle };
@@ -259,6 +259,9 @@ var SonicValueElement = class extends SonicFormElement {
259
259
  this.upgradeProperties("default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "spokenStep", "step", "taper", "positions", "detent", "formatEntry", "formatValue", "formatSpokenValue", "keyStep", "parseValue", "value");
260
260
  super.connectedCallback();
261
261
  }
262
+ abandonHold() {
263
+ this.#gestures?.abandon();
264
+ }
262
265
  bindGestures(control, signal, grab) {
263
266
  const bubble = control.querySelector("[popover]");
264
267
  if (bubble) this.#readout = {
@@ -27,6 +27,13 @@ var ValueGestures = class {
27
27
  this.#host = host;
28
28
  this.#control = control;
29
29
  }
30
+ abandon() {
31
+ const drag = this.#dragging();
32
+ if (!drag) return;
33
+ this.#host.input(drag.fromValue);
34
+ this.#entry?.forgetPress();
35
+ this.#pointerDrag?.end();
36
+ }
30
37
  bind(signal, grab) {
31
38
  const control = this.#control;
32
39
  const host = this.#host;
@@ -63,7 +63,14 @@ var SonicWaveElement = class extends SonicValueElement {
63
63
  signal
64
64
  });
65
65
  this.render();
66
- this.checkStyles(control, this.sheet);
66
+ this.checkStyles(control, this.sheet, [
67
+ "cap",
68
+ "glass",
69
+ "pane",
70
+ "well",
71
+ "readout",
72
+ "value"
73
+ ]);
67
74
  this.#syncFill = bindFill(control, this.sizeProperty, signal);
68
75
  this.#syncFill(this.fill);
69
76
  }