@xsynaptic/sonic-ui 0.5.0 → 0.6.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 (118) hide show
  1. package/README.md +3 -1
  2. package/dist/default/dat.js +5 -4
  3. package/dist/default/define/button.js +1 -1
  4. package/dist/default/define.js +2 -2
  5. package/dist/default/elements/button.js +3 -3
  6. package/dist/default/elements/envelope.js +32 -16
  7. package/dist/default/elements/meter.js +11 -7
  8. package/dist/default/elements/number.js +1 -1
  9. package/dist/default/elements/radio-group.js +43 -45
  10. package/dist/default/elements/readout.js +1 -1
  11. package/dist/default/elements/sonic-element.js +56 -29
  12. package/dist/default/elements/switch.js +32 -38
  13. package/dist/default/elements/value-element.js +111 -108
  14. package/dist/default/elements/wave-element.js +5 -2
  15. package/dist/default/elements/waveform.js +35 -12
  16. package/dist/default/elements/wavestrip.js +18 -15
  17. package/dist/default/elements/xy.js +32 -18
  18. package/dist/default/index.js +2 -2
  19. package/dist/default/lib/adsr-shape.js +5 -0
  20. package/dist/default/lib/ballistics.js +2 -2
  21. package/dist/default/lib/canvas-surface.js +13 -2
  22. package/dist/default/lib/distribute.js +1 -1
  23. package/dist/default/lib/drag-step.js +4 -4
  24. package/dist/default/lib/frame-timeline.js +2 -1
  25. package/dist/default/lib/marker-band.js +54 -10
  26. package/dist/default/lib/marker-rider.js +10 -4
  27. package/dist/default/lib/math.js +6 -1
  28. package/dist/default/lib/pointer-drag.js +8 -5
  29. package/dist/default/lib/resample-peaks.js +11 -1
  30. package/dist/default/lib/write-attribute.js +1 -0
  31. package/dist/dev/dat.d.ts +2 -2
  32. package/dist/dev/dat.js +5 -4
  33. package/dist/dev/dat.js.map +1 -1
  34. package/dist/dev/define/button.js +1 -1
  35. package/dist/dev/define.d.ts +4 -4
  36. package/dist/dev/define.js +2 -2
  37. package/dist/dev/elements/button.js +3 -3
  38. package/dist/dev/elements/button.js.map +1 -1
  39. package/dist/dev/elements/dial.d.ts +1 -1
  40. package/dist/dev/elements/dial.js.map +1 -1
  41. package/dist/dev/elements/envelope.js +32 -16
  42. package/dist/dev/elements/envelope.js.map +1 -1
  43. package/dist/dev/elements/meter.d.ts +1 -1
  44. package/dist/dev/elements/meter.js +11 -7
  45. package/dist/dev/elements/meter.js.map +1 -1
  46. package/dist/dev/elements/number.js +1 -1
  47. package/dist/dev/elements/number.js.map +1 -1
  48. package/dist/dev/elements/radio-group.js +43 -45
  49. package/dist/dev/elements/radio-group.js.map +1 -1
  50. package/dist/dev/elements/readout.js +1 -1
  51. package/dist/dev/elements/readout.js.map +1 -1
  52. package/dist/dev/elements/slider.d.ts +1 -1
  53. package/dist/dev/elements/slider.js.map +1 -1
  54. package/dist/dev/elements/sonic-element.d.ts +4 -2
  55. package/dist/dev/elements/sonic-element.js +79 -29
  56. package/dist/dev/elements/sonic-element.js.map +1 -1
  57. package/dist/dev/elements/switch.js +32 -38
  58. package/dist/dev/elements/switch.js.map +1 -1
  59. package/dist/dev/elements/value-element.d.ts +3 -3
  60. package/dist/dev/elements/value-element.js +111 -108
  61. package/dist/dev/elements/value-element.js.map +1 -1
  62. package/dist/dev/elements/wave-element.d.ts +1 -0
  63. package/dist/dev/elements/wave-element.js +5 -2
  64. package/dist/dev/elements/wave-element.js.map +1 -1
  65. package/dist/dev/elements/waveform.d.ts +1 -1
  66. package/dist/dev/elements/waveform.js +35 -12
  67. package/dist/dev/elements/waveform.js.map +1 -1
  68. package/dist/dev/elements/wavestrip.js +18 -15
  69. package/dist/dev/elements/wavestrip.js.map +1 -1
  70. package/dist/dev/elements/xy.d.ts +3 -3
  71. package/dist/dev/elements/xy.js +32 -18
  72. package/dist/dev/elements/xy.js.map +1 -1
  73. package/dist/dev/index.d.ts +2 -2
  74. package/dist/dev/index.js +2 -2
  75. package/dist/dev/lib/adsr-shape.js +5 -0
  76. package/dist/dev/lib/adsr-shape.js.map +1 -1
  77. package/dist/dev/lib/ballistics.js +2 -2
  78. package/dist/dev/lib/ballistics.js.map +1 -1
  79. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  80. package/dist/dev/lib/canvas-surface.js +13 -2
  81. package/dist/dev/lib/canvas-surface.js.map +1 -1
  82. package/dist/dev/lib/distribute.js +1 -1
  83. package/dist/dev/lib/distribute.js.map +1 -1
  84. package/dist/dev/lib/drag-step.js +4 -4
  85. package/dist/dev/lib/drag-step.js.map +1 -1
  86. package/dist/dev/lib/frame-timeline.js +2 -1
  87. package/dist/dev/lib/frame-timeline.js.map +1 -1
  88. package/dist/dev/lib/marker-band.js +54 -10
  89. package/dist/dev/lib/marker-band.js.map +1 -1
  90. package/dist/dev/lib/marker-rider.js +10 -4
  91. package/dist/dev/lib/marker-rider.js.map +1 -1
  92. package/dist/dev/lib/math.js +6 -1
  93. package/dist/dev/lib/math.js.map +1 -1
  94. package/dist/dev/lib/pointer-drag.js +8 -5
  95. package/dist/dev/lib/pointer-drag.js.map +1 -1
  96. package/dist/dev/lib/resample-peaks.js +11 -1
  97. package/dist/dev/lib/resample-peaks.js.map +1 -1
  98. package/dist/dev/lib/write-attribute.js +1 -0
  99. package/dist/dev/lib/write-attribute.js.map +1 -1
  100. package/dist/skins/amber.css +8 -0
  101. package/dist/skins/flat.css +25 -0
  102. package/dist/skins/ivory.css +1 -0
  103. package/dist/skins/lime.css +2 -0
  104. package/dist/skins/slate.css +2 -0
  105. package/dist/styles/button.css +38 -10
  106. package/dist/styles/dial.css +24 -16
  107. package/dist/styles/envelope.css +56 -11
  108. package/dist/styles/led.css +3 -3
  109. package/dist/styles/material.css +83 -41
  110. package/dist/styles/meter.css +8 -4
  111. package/dist/styles/panel.css +5 -3
  112. package/dist/styles/segmented.css +27 -5
  113. package/dist/styles/slider.css +2 -1
  114. package/dist/styles/switch.css +59 -20
  115. package/dist/styles/waveform.css +28 -5
  116. package/dist/styles/wavestrip.css +23 -6
  117. package/dist/styles/xy.css +61 -24
  118. package/package.json +2 -5
@@ -2,6 +2,7 @@ import { clamp } from "../lib/math.js";
2
2
  import { requireChild, template } from "../lib/render.js";
3
3
  import { writeAttribute } from "../lib/write-attribute.js";
4
4
  import { bindHold } from "../lib/hold.js";
5
+ import { bindDrag } from "../lib/pointer-drag.js";
5
6
  import { SonicRadioGroupElement, isUnder, optionValue } from "./radio-group.js";
6
7
 
7
8
  //#region src/elements/switch.ts
@@ -60,7 +61,6 @@ var SonicSwitch = class extends SonicRadioGroupElement {
60
61
  group = renderSwitch();
61
62
  #bare = renderBare();
62
63
  #bat = requireChild(this.group, ".sonic-switch-bat", HTMLSpanElement);
63
- #batPress;
64
64
  #checked = false;
65
65
  #holding;
66
66
  #isClickSwallowed = false;
@@ -92,7 +92,7 @@ var SonicSwitch = class extends SonicRadioGroupElement {
92
92
  option.focus();
93
93
  }
94
94
  claimPress(option, event) {
95
- if (this.#grabBat(event)) return true;
95
+ if (this.#isOnBat(event)) return true;
96
96
  if (!this.#isMomentary(option)) return false;
97
97
  this.group.setPointerCapture(event.pointerId);
98
98
  this.#hold(option, event.pointerId);
@@ -108,9 +108,8 @@ var SonicSwitch = class extends SonicRadioGroupElement {
108
108
  this.#isClickSwallowed = false;
109
109
  if (!isSwallowed) this.#setChecked(!this.checked);
110
110
  }, { signal });
111
- this.#bare.addEventListener("pointerdown", (event) => {
111
+ this.#bare.addEventListener("pointerdown", () => {
112
112
  this.#isClickSwallowed = false;
113
- if (event.button === 0 && !this.isDisabled()) this.#grabBat(event);
114
113
  }, { signal });
115
114
  this.#bindBat(group, signal);
116
115
  this.#holding = bindHold(group, {
@@ -138,12 +137,13 @@ var SonicSwitch = class extends SonicRadioGroupElement {
138
137
  return option;
139
138
  }
140
139
  render() {
140
+ if (!this.isBound()) return;
141
141
  const group = this.group;
142
142
  const options = this.options();
143
143
  if (options.length > 0) {
144
144
  this.#bare.remove();
145
145
  writeAttribute(group, "role", "radiogroup");
146
- group.dataset.sonicPositionCount = String(options.length);
146
+ writeAttribute(group, "data-sonic-position-count", String(options.length));
147
147
  super.render();
148
148
  this.#renderThrow(this.#checkedIndex(options));
149
149
  return;
@@ -153,8 +153,8 @@ var SonicSwitch = class extends SonicRadioGroupElement {
153
153
  writeAttribute(group, "role", void 0);
154
154
  delete group.dataset.sonicPositionCount;
155
155
  if (bare.parentElement !== group) group.append(bare);
156
- bare.disabled = this.isDisabled();
157
- bare.setAttribute("aria-checked", String(isChecked));
156
+ writeAttribute(bare, "disabled", this.isDisabled() ? "" : void 0);
157
+ writeAttribute(bare, "aria-checked", String(isChecked));
158
158
  this.forwardNaming(group, false);
159
159
  this.forwardNaming(bare, true);
160
160
  const onSide = this.orientation === "horizontal" ? 1 : -1;
@@ -172,41 +172,32 @@ var SonicSwitch = class extends SonicRadioGroupElement {
172
172
  return this.orientation === "horizontal" ? event.clientX : event.clientY;
173
173
  }
174
174
  #bindBat(group, signal) {
175
- const drop = (event) => {
176
- if (event.pointerId === this.#batPress?.pointerId) this.#batPress = void 0;
177
- };
178
- group.addEventListener("pointermove", (event) => {
179
- const press = this.#batPress;
180
- if (press?.pointerId !== event.pointerId || this.isDisabled()) return;
181
- const delta = this.#along(event) - press.last;
182
- if (Math.abs(delta) < this.#bat.getBoundingClientRect().width / 4) return;
183
- press.hasMoved = true;
184
- press.last = this.#along(event);
185
- this.#throwBy(Math.sign(delta), event.pointerId);
186
- }, { signal });
187
- group.addEventListener("pointerup", (event) => {
188
- const press = this.#batPress;
189
- if (press?.pointerId !== event.pointerId) return;
190
- this.#batPress = void 0;
191
- if (!press.hasMoved && !this.isDisabled()) this.#pressBat(event);
192
- this.#isClickSwallowed = true;
193
- }, { signal });
194
- group.addEventListener("pointercancel", drop, { signal });
195
- group.addEventListener("lostpointercapture", drop, { signal });
175
+ bindDrag(group, {
176
+ grab: (event) => {
177
+ if (!(event.target === this.#bare || this.optionOf(event.target) !== void 0) || this.isDisabled() || !this.#isOnBat(event)) return;
178
+ return {
179
+ hasMoved: false,
180
+ last: this.#along(event),
181
+ width: this.#bat.getBoundingClientRect().width
182
+ };
183
+ },
184
+ lift: (press, event) => {
185
+ if (!press.hasMoved && !this.isDisabled()) this.#pressBat(event);
186
+ this.#isClickSwallowed = true;
187
+ },
188
+ move: (press, event) => {
189
+ if (this.isDisabled()) return;
190
+ const delta = this.#along(event) - press.last;
191
+ if (Math.abs(delta) < press.width / 4) return;
192
+ press.hasMoved = true;
193
+ press.last = this.#along(event);
194
+ this.#throwBy(Math.sign(delta), event.pointerId);
195
+ }
196
+ }, signal);
196
197
  }
197
198
  #checkedIndex(options) {
198
199
  return options.findIndex((option) => optionValue(option) === this.value);
199
200
  }
200
- #grabBat(event) {
201
- if (!isUnder(this.#bat, event.clientX, event.clientY)) return false;
202
- this.group.setPointerCapture(event.pointerId);
203
- this.#batPress = {
204
- hasMoved: false,
205
- last: this.#along(event),
206
- pointerId: event.pointerId
207
- };
208
- return true;
209
- }
210
201
  #hasSprungFrom(option) {
211
202
  if (!option || !this.#isMomentary(option)) return false;
212
203
  this.#holding?.release();
@@ -222,6 +213,9 @@ var SonicSwitch = class extends SonicRadioGroupElement {
222
213
  const options = this.options();
223
214
  return (option === options[0] || option === options.at(-1)) && options.length > 1 && option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-momentary") === true;
224
215
  }
216
+ #isOnBat(event) {
217
+ return isUnder(this.#bat, event.clientX, event.clientY);
218
+ }
225
219
  #pressBat(event) {
226
220
  const options = this.options();
227
221
  if (options.length === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"switch.js","names":[],"sources":["../../../src/elements/switch.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { isUnder, optionValue, SonicRadioGroupElement } from '#elements/radio-group.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { clamp } from '#lib/math.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-switch': SonicSwitch;\n\t}\n}\n\nconst renderSwitch = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-switch\">\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-switch-bushing\"></span>\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-switch-bat\"></span>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst renderPosition = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-switch-position\" role=\"radio\" type=\"button\">\n\t\t\t<span class=\"sonic-switch-label\"></span>\n\t\t</button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst renderBare = template(\n\t/* HTML */ `<button class=\"sonic-switch-position\" role=\"switch\" type=\"button\"></button>`,\n\tHTMLButtonElement,\n);\n\ninterface BatPress {\n\thasMoved: boolean;\n\tlast: number;\n\tpointerId: number;\n}\n\nconst pressCentreZone = 0.08;\n\nfunction pressedIndex(index: number, count: number, offset: number): number {\n\tif (count === 2) return index === 0 ? 1 : 0;\n\n\tconst side = Math.abs(offset) < pressCentreZone ? 1 : Math.sign(offset);\n\tconst next = index + side;\n\n\treturn next < 0 || next >= count ? index - side : next;\n}\n\nexport class SonicSwitch extends SonicRadioGroupElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicRadioGroupElement.observedAttributes,\n\t\t'checked',\n\t\t'orientation',\n\t];\n\n\tget checked(): boolean {\n\t\treturn this.#isDirty ? this.#checked : this.defaultChecked;\n\t}\n\n\tset checked(isChecked: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#checked = isChecked === true;\n\t\tthis.render();\n\t}\n\n\tget defaultChecked(): boolean {\n\t\treturn this.hasAttribute('checked');\n\t}\n\n\tset defaultChecked(isChecked: boolean) {\n\t\tthis.reflect('checked', isChecked);\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute\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\toverride get value(): string {\n\t\tconst value = super.value;\n\n\t\treturn value === '' && this.#isBare() ? 'on' : value;\n\t}\n\n\toverride set value(next: string) {\n\t\tsuper.value = next;\n\t}\n\n\tprotected readonly group = renderSwitch();\n\n\treadonly #bare = renderBare();\n\n\treadonly #bat = requireChild(this.group, '.sonic-switch-bat', HTMLSpanElement);\n\n\t#batPress: BatPress | undefined;\n\n\t#checked = false;\n\n\t#holding: Hold | undefined;\n\n\t#isClickSwallowed = false;\n\n\t#isDirty = false;\n\n\toverride attributeChangedCallback(name: string): void {\n\t\tif (name === 'checked') this.#isDirty = false;\n\t\tif (this.isDisabled()) this.#holding?.release();\n\t\tsuper.attributeChangedCallback(name);\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.#isDirty = false;\n\t\tsuper.formResetCallback();\n\t}\n\n\tprotected override activate(): void {\n\t\tif (this.#isBare()) this.#bare.click();\n\t\telse super.activate();\n\t}\n\n\tprotected override chooseByClick(option: HTMLButtonElement): void {\n\t\tsuper.chooseByClick(option);\n\t\tif (this.#isMomentary(option)) this.#springBack();\n\t}\n\n\tprotected override chooseByKey(option: HTMLButtonElement, event: KeyboardEvent): void {\n\t\tif (!this.#isMomentary(option)) {\n\t\t\tsuper.chooseByKey(option, event);\n\t\t\treturn;\n\t\t}\n\t\tif (event.repeat) return;\n\n\t\tthis.#hold(option, event.key);\n\t\toption.focus();\n\t}\n\n\tprotected override claimPress(option: HTMLButtonElement, event: PointerEvent): boolean {\n\t\tif (this.#grabBat(event)) return true;\n\t\tif (!this.#isMomentary(option)) return false;\n\n\t\tthis.group.setPointerCapture(event.pointerId);\n\t\tthis.#hold(option, event.pointerId);\n\n\t\treturn true;\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tconst group = this.group;\n\n\t\tthis.upgradeProperties('defaultChecked', 'checked', 'orientation');\n\t\tsuper.connect(signal);\n\t\tthis.checkStyles(group, 'switch.css');\n\n\t\tthis.#bare.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\tconst isSwallowed =\n\t\t\t\t\tthis.#isClickSwallowed && event instanceof PointerEvent && event.pointerType !== '';\n\n\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t\tif (!isSwallowed) this.#setChecked(!this.checked);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bare.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t\tif (event.button === 0 && !this.isDisabled()) this.#grabBat(event);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindBat(group, signal);\n\n\t\tthis.#holding = bindHold(\n\t\t\tgroup,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled(),\n\t\t\t\tleave: 'focusout',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.toggleState('held', true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.toggleState('held', false);\n\t\t\t\t\tthis.#springBack();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\tprotected override focusTarget(): HTMLElement | undefined {\n\t\treturn this.#isBare() ? this.#bare : super.focusTarget();\n\t}\n\n\tprotected override isWrapping(): boolean {\n\t\treturn false;\n\t}\n\n\tprotected override releaseTarget(option: HTMLButtonElement): HTMLButtonElement | undefined {\n\t\tif (this.#isMomentary(option)) return undefined;\n\n\t\tconst options = this.options();\n\t\tif (options.length === 2 && optionValue(option) === this.value) {\n\t\t\treturn options.find((other) => other !== option);\n\t\t}\n\n\t\treturn option;\n\t}\n\n\tprotected override render(): void {\n\t\tconst group = this.group;\n\t\tconst options = this.options();\n\n\t\tif (options.length > 0) {\n\t\t\tthis.#bare.remove();\n\t\t\twriteAttribute(group, 'role', 'radiogroup');\n\t\t\tgroup.dataset.sonicPositionCount = String(options.length);\n\t\t\tsuper.render();\n\t\t\tthis.#renderThrow(this.#checkedIndex(options));\n\t\t\treturn;\n\t\t}\n\n\t\tconst bare = this.#bare;\n\t\tconst isChecked = this.checked;\n\n\t\twriteAttribute(group, 'role', undefined);\n\t\tdelete group.dataset.sonicPositionCount;\n\t\tif (bare.parentElement !== group) group.append(bare);\n\t\tbare.disabled = this.isDisabled();\n\t\tbare.setAttribute('aria-checked', String(isChecked));\n\t\tthis.forwardNaming(group, false);\n\t\tthis.forwardNaming(bare, true);\n\t\tconst onSide = this.orientation === 'horizontal' ? 1 : -1;\n\n\t\tgroup.style.setProperty('--_sonic-switch-at', String(isChecked ? onSide : -onSide));\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tthis.writeFormValue(isChecked ? this.value : null, String(isChecked));\n\t}\n\n\tprotected renderOption(): HTMLButtonElement {\n\t\treturn renderPosition();\n\t}\n\n\tprotected override restoreState(state: string): void {\n\t\tif (this.#isBare()) this.checked = state === 'true';\n\t\telse super.restoreState(state);\n\t}\n\n\t#along(event: PointerEvent): number {\n\t\treturn this.orientation === 'horizontal' ? event.clientX : event.clientY;\n\t}\n\n\t#bindBat(group: HTMLElement, signal: AbortSignal): void {\n\t\tconst drop = (event: PointerEvent): void => {\n\t\t\tif (event.pointerId === this.#batPress?.pointerId) this.#batPress = undefined;\n\t\t};\n\n\t\tgroup.addEventListener(\n\t\t\t'pointermove',\n\t\t\t(event) => {\n\t\t\t\tconst press = this.#batPress;\n\t\t\t\tif (press?.pointerId !== event.pointerId || this.isDisabled()) return;\n\n\t\t\t\tconst delta = this.#along(event) - press.last;\n\t\t\t\tif (Math.abs(delta) < this.#bat.getBoundingClientRect().width / 4) return;\n\n\t\t\t\tpress.hasMoved = true;\n\t\t\t\tpress.last = this.#along(event);\n\t\t\t\tthis.#throwBy(Math.sign(delta), event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tgroup.addEventListener(\n\t\t\t'pointerup',\n\t\t\t(event) => {\n\t\t\t\tconst press = this.#batPress;\n\t\t\t\tif (press?.pointerId !== event.pointerId) return;\n\n\t\t\t\tthis.#batPress = undefined;\n\t\t\t\tif (!press.hasMoved && !this.isDisabled()) this.#pressBat(event);\n\t\t\t\tthis.#isClickSwallowed = true;\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tgroup.addEventListener('pointercancel', drop, { signal });\n\t\tgroup.addEventListener('lostpointercapture', drop, { signal });\n\t}\n\n\t#checkedIndex(options: Array<HTMLButtonElement>): number {\n\t\treturn options.findIndex((option) => optionValue(option) === this.value);\n\t}\n\n\t#grabBat(event: PointerEvent): boolean {\n\t\tif (!isUnder(this.#bat, event.clientX, event.clientY)) return false;\n\n\t\tthis.group.setPointerCapture(event.pointerId);\n\t\tthis.#batPress = { hasMoved: false, last: this.#along(event), pointerId: event.pointerId };\n\n\t\treturn true;\n\t}\n\n\t#hasSprungFrom(option: HTMLButtonElement | undefined): boolean {\n\t\tif (!option || !this.#isMomentary(option)) return false;\n\n\t\tthis.#holding?.release();\n\n\t\treturn this.value !== optionValue(option);\n\t}\n\n\t#hold(option: HTMLButtonElement, by: number | string): void {\n\t\tif (this.#holding?.hold(by) === true) this.select(option);\n\t}\n\n\t#isBare(): boolean {\n\t\treturn this.options().length === 0;\n\t}\n\n\t#isMomentary(option: HTMLButtonElement): boolean {\n\t\tconst options = this.options();\n\t\tconst isEnd = option === options[0] || option === options.at(-1);\n\n\t\treturn (\n\t\t\tisEnd &&\n\t\t\toptions.length > 1 &&\n\t\t\toption.querySelector('[data-sonic-value]')?.hasAttribute('data-sonic-momentary') === true\n\t\t);\n\t}\n\n\t#pressBat(event: PointerEvent): void {\n\t\tconst options = this.options();\n\t\tif (options.length === 0) {\n\t\t\tthis.#setChecked(!this.checked);\n\t\t\treturn;\n\t\t}\n\n\t\tconst box = this.#bat.getBoundingClientRect();\n\t\tconst [start, size] =\n\t\t\tthis.orientation === 'horizontal' ? [box.left, box.width] : [box.top, box.height];\n\t\tconst offset = (this.#along(event) - start) / size - 0.5;\n\t\tconst option = options[pressedIndex(this.#checkedIndex(options), options.length, offset)];\n\t\tif (!option) return;\n\n\t\tthis.chooseByClick(option);\n\t\tthis.#refocus(options);\n\t}\n\n\t#refocus(options: Array<HTMLButtonElement>): void {\n\t\tif (this.group.matches(':focus-within')) options[this.#checkedIndex(options)]?.focus();\n\t}\n\n\t#renderThrow(index: number): void {\n\t\tconst count = this.options().length;\n\t\tconst at = index < 0 || count < 2 ? 0 : (2 * index) / (count - 1) - 1;\n\n\t\tthis.group.style.setProperty('--_sonic-switch-at', String(at));\n\t}\n\n\t#setChecked(isChecked: boolean): void {\n\t\tif (isChecked === this.checked) return;\n\n\t\tthis.checked = isChecked;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#springBack(): void {\n\t\tconst options = this.options();\n\t\tconst index = this.#checkedIndex(options);\n\t\tconst held = options[index];\n\t\tif (!held || !this.#isMomentary(held)) return;\n\n\t\tconst rest = options[index === 0 ? 1 : index - 1];\n\t\tif (!rest) return;\n\n\t\tconst isFocused = this.group.contains(this.ownerDocument.activeElement);\n\n\t\tthis.select(rest);\n\t\tif (isFocused) rest.focus();\n\t}\n\n\t#throwBy(direction: number, pointerId: number): void {\n\t\tconst options = this.options();\n\t\tif (options.length === 0) {\n\t\t\tthis.#setChecked(direction === (this.orientation === 'horizontal' ? 1 : -1));\n\t\t\treturn;\n\t\t}\n\n\t\tconst index = this.#checkedIndex(options);\n\t\tconst option = options[clamp(index + direction, 0, options.length - 1)];\n\t\tif (!option || option === options[index] || this.#hasSprungFrom(options[index])) return;\n\n\t\tif (this.#isMomentary(option)) this.#hold(option, pointerId);\n\t\telse this.select(option);\n\t\tthis.#refocus(options);\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"switch.js","names":[],"sources":["../../../src/elements/switch.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { isUnder, optionValue, SonicRadioGroupElement } from '#elements/radio-group.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { clamp } from '#lib/math.ts';\nimport { bindDrag } from '#lib/pointer-drag.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-switch': SonicSwitch;\n\t}\n}\n\nconst renderSwitch = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-switch\">\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-switch-bushing\"></span>\n\t\t\t<span aria-hidden=\"true\" class=\"sonic-switch-bat\"></span>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nconst renderPosition = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-switch-position\" role=\"radio\" type=\"button\">\n\t\t\t<span class=\"sonic-switch-label\"></span>\n\t\t</button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst renderBare = template(\n\t/* HTML */ `<button class=\"sonic-switch-position\" role=\"switch\" type=\"button\"></button>`,\n\tHTMLButtonElement,\n);\n\ninterface BatPress {\n\thasMoved: boolean;\n\tlast: number;\n\twidth: number;\n}\n\nconst pressCentreZone = 0.08;\n\nfunction pressedIndex(index: number, count: number, offset: number): number {\n\tif (count === 2) return index === 0 ? 1 : 0;\n\n\tconst side = Math.abs(offset) < pressCentreZone ? 1 : Math.sign(offset);\n\tconst next = index + side;\n\n\treturn next < 0 || next >= count ? index - side : next;\n}\n\nexport class SonicSwitch extends SonicRadioGroupElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicRadioGroupElement.observedAttributes,\n\t\t'checked',\n\t\t'orientation',\n\t];\n\n\tget checked(): boolean {\n\t\treturn this.#isDirty ? this.#checked : this.defaultChecked;\n\t}\n\n\tset checked(isChecked: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#checked = isChecked === true;\n\t\tthis.render();\n\t}\n\n\tget defaultChecked(): boolean {\n\t\treturn this.hasAttribute('checked');\n\t}\n\n\tset defaultChecked(isChecked: boolean) {\n\t\tthis.reflect('checked', isChecked);\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute\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\toverride get value(): string {\n\t\tconst value = super.value;\n\n\t\treturn value === '' && this.#isBare() ? 'on' : value;\n\t}\n\n\toverride set value(next: string) {\n\t\tsuper.value = next;\n\t}\n\n\tprotected readonly group = renderSwitch();\n\n\treadonly #bare = renderBare();\n\n\treadonly #bat = requireChild(this.group, '.sonic-switch-bat', HTMLSpanElement);\n\n\t#checked = false;\n\n\t#holding: Hold | undefined;\n\n\t#isClickSwallowed = false;\n\n\t#isDirty = false;\n\n\toverride attributeChangedCallback(name: string): void {\n\t\tif (name === 'checked') this.#isDirty = false;\n\t\tif (this.isDisabled()) this.#holding?.release();\n\t\tsuper.attributeChangedCallback(name);\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.#isDirty = false;\n\t\tsuper.formResetCallback();\n\t}\n\n\tprotected override activate(): void {\n\t\tif (this.#isBare()) this.#bare.click();\n\t\telse super.activate();\n\t}\n\n\tprotected override chooseByClick(option: HTMLButtonElement): void {\n\t\tsuper.chooseByClick(option);\n\t\tif (this.#isMomentary(option)) this.#springBack();\n\t}\n\n\tprotected override chooseByKey(option: HTMLButtonElement, event: KeyboardEvent): void {\n\t\tif (!this.#isMomentary(option)) {\n\t\t\tsuper.chooseByKey(option, event);\n\t\t\treturn;\n\t\t}\n\t\tif (event.repeat) return;\n\n\t\tthis.#hold(option, event.key);\n\t\toption.focus();\n\t}\n\n\tprotected override claimPress(option: HTMLButtonElement, event: PointerEvent): boolean {\n\t\tif (this.#isOnBat(event)) return true;\n\t\tif (!this.#isMomentary(option)) return false;\n\n\t\tthis.group.setPointerCapture(event.pointerId);\n\t\tthis.#hold(option, event.pointerId);\n\n\t\treturn true;\n\t}\n\n\tprotected override connect(signal: AbortSignal): void {\n\t\tconst group = this.group;\n\n\t\tthis.upgradeProperties('defaultChecked', 'checked', 'orientation');\n\t\tsuper.connect(signal);\n\t\tthis.checkStyles(group, 'switch.css');\n\n\t\tthis.#bare.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\tconst isSwallowed =\n\t\t\t\t\tthis.#isClickSwallowed && event instanceof PointerEvent && event.pointerType !== '';\n\n\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t\tif (!isSwallowed) this.#setChecked(!this.checked);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bare.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t() => {\n\t\t\t\tthis.#isClickSwallowed = false;\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindBat(group, signal);\n\n\t\tthis.#holding = bindHold(\n\t\t\tgroup,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled(),\n\t\t\t\tleave: 'focusout',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.toggleState('held', true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.toggleState('held', false);\n\t\t\t\t\tthis.#springBack();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\tprotected override focusTarget(): HTMLElement | undefined {\n\t\treturn this.#isBare() ? this.#bare : super.focusTarget();\n\t}\n\n\tprotected override isWrapping(): boolean {\n\t\treturn false;\n\t}\n\n\tprotected override releaseTarget(option: HTMLButtonElement): HTMLButtonElement | undefined {\n\t\tif (this.#isMomentary(option)) return undefined;\n\n\t\tconst options = this.options();\n\t\tif (options.length === 2 && optionValue(option) === this.value) {\n\t\t\treturn options.find((other) => other !== option);\n\t\t}\n\n\t\treturn option;\n\t}\n\n\tprotected override render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst group = this.group;\n\t\tconst options = this.options();\n\n\t\tif (options.length > 0) {\n\t\t\tthis.#bare.remove();\n\t\t\twriteAttribute(group, 'role', 'radiogroup');\n\t\t\twriteAttribute(group, 'data-sonic-position-count', String(options.length));\n\t\t\tsuper.render();\n\t\t\tthis.#renderThrow(this.#checkedIndex(options));\n\t\t\treturn;\n\t\t}\n\n\t\tconst bare = this.#bare;\n\t\tconst isChecked = this.checked;\n\n\t\twriteAttribute(group, 'role', undefined);\n\t\tdelete group.dataset.sonicPositionCount;\n\t\tif (bare.parentElement !== group) group.append(bare);\n\t\twriteAttribute(bare, 'disabled', this.isDisabled() ? '' : undefined);\n\t\twriteAttribute(bare, 'aria-checked', String(isChecked));\n\t\tthis.forwardNaming(group, false);\n\t\tthis.forwardNaming(bare, true);\n\t\tconst onSide = this.orientation === 'horizontal' ? 1 : -1;\n\n\t\tgroup.style.setProperty('--_sonic-switch-at', String(isChecked ? onSide : -onSide));\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tthis.writeFormValue(isChecked ? this.value : null, String(isChecked));\n\t}\n\n\tprotected renderOption(): HTMLButtonElement {\n\t\treturn renderPosition();\n\t}\n\n\tprotected override restoreState(state: string): void {\n\t\tif (this.#isBare()) this.checked = state === 'true';\n\t\telse super.restoreState(state);\n\t}\n\n\t#along(event: PointerEvent): number {\n\t\treturn this.orientation === 'horizontal' ? event.clientX : event.clientY;\n\t}\n\n\t#bindBat(group: HTMLElement, signal: AbortSignal): void {\n\t\tbindDrag<BatPress>(\n\t\t\tgroup,\n\t\t\t{\n\t\t\t\tgrab: (event) => {\n\t\t\t\t\tconst isOnPosition =\n\t\t\t\t\t\tevent.target === this.#bare || this.optionOf(event.target) !== undefined;\n\t\t\t\t\tif (!isOnPosition || this.isDisabled() || !this.#isOnBat(event)) return;\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\thasMoved: false,\n\t\t\t\t\t\tlast: this.#along(event),\n\t\t\t\t\t\twidth: this.#bat.getBoundingClientRect().width,\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t\tlift: (press, event) => {\n\t\t\t\t\tif (!press.hasMoved && !this.isDisabled()) this.#pressBat(event);\n\t\t\t\t\tthis.#isClickSwallowed = true;\n\t\t\t\t},\n\t\t\t\tmove: (press, event) => {\n\t\t\t\t\tif (this.isDisabled()) return;\n\n\t\t\t\t\tconst delta = this.#along(event) - press.last;\n\t\t\t\t\tif (Math.abs(delta) < press.width / 4) return;\n\n\t\t\t\t\tpress.hasMoved = true;\n\t\t\t\t\tpress.last = this.#along(event);\n\t\t\t\t\tthis.#throwBy(Math.sign(delta), event.pointerId);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t}\n\n\t#checkedIndex(options: Array<HTMLButtonElement>): number {\n\t\treturn options.findIndex((option) => optionValue(option) === this.value);\n\t}\n\n\t#hasSprungFrom(option: HTMLButtonElement | undefined): boolean {\n\t\tif (!option || !this.#isMomentary(option)) return false;\n\n\t\tthis.#holding?.release();\n\n\t\treturn this.value !== optionValue(option);\n\t}\n\n\t#hold(option: HTMLButtonElement, by: number | string): void {\n\t\tif (this.#holding?.hold(by) === true) this.select(option);\n\t}\n\n\t#isBare(): boolean {\n\t\treturn this.options().length === 0;\n\t}\n\n\t#isMomentary(option: HTMLButtonElement): boolean {\n\t\tconst options = this.options();\n\t\tconst isEnd = option === options[0] || option === options.at(-1);\n\n\t\treturn (\n\t\t\tisEnd &&\n\t\t\toptions.length > 1 &&\n\t\t\toption.querySelector('[data-sonic-value]')?.hasAttribute('data-sonic-momentary') === true\n\t\t);\n\t}\n\n\t#isOnBat(event: PointerEvent): boolean {\n\t\treturn isUnder(this.#bat, event.clientX, event.clientY);\n\t}\n\n\t#pressBat(event: PointerEvent): void {\n\t\tconst options = this.options();\n\t\tif (options.length === 0) {\n\t\t\tthis.#setChecked(!this.checked);\n\t\t\treturn;\n\t\t}\n\n\t\tconst box = this.#bat.getBoundingClientRect();\n\t\tconst [start, size] =\n\t\t\tthis.orientation === 'horizontal' ? [box.left, box.width] : [box.top, box.height];\n\t\tconst offset = (this.#along(event) - start) / size - 0.5;\n\t\tconst option = options[pressedIndex(this.#checkedIndex(options), options.length, offset)];\n\t\tif (!option) return;\n\n\t\tthis.chooseByClick(option);\n\t\tthis.#refocus(options);\n\t}\n\n\t#refocus(options: Array<HTMLButtonElement>): void {\n\t\tif (this.group.matches(':focus-within')) options[this.#checkedIndex(options)]?.focus();\n\t}\n\n\t#renderThrow(index: number): void {\n\t\tconst count = this.options().length;\n\t\tconst at = index < 0 || count < 2 ? 0 : (2 * index) / (count - 1) - 1;\n\n\t\tthis.group.style.setProperty('--_sonic-switch-at', String(at));\n\t}\n\n\t#setChecked(isChecked: boolean): void {\n\t\tif (isChecked === this.checked) return;\n\n\t\tthis.checked = isChecked;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#springBack(): void {\n\t\tconst options = this.options();\n\t\tconst index = this.#checkedIndex(options);\n\t\tconst held = options[index];\n\t\tif (!held || !this.#isMomentary(held)) return;\n\n\t\tconst rest = options[index === 0 ? 1 : index - 1];\n\t\tif (!rest) return;\n\n\t\tconst isFocused = this.group.contains(this.ownerDocument.activeElement);\n\n\t\tthis.select(rest);\n\t\tif (isFocused) rest.focus();\n\t}\n\n\t#throwBy(direction: number, pointerId: number): void {\n\t\tconst options = this.options();\n\t\tif (options.length === 0) {\n\t\t\tthis.#setChecked(direction === (this.orientation === 'horizontal' ? 1 : -1));\n\t\t\treturn;\n\t\t}\n\n\t\tconst index = this.#checkedIndex(options);\n\t\tconst option = options[clamp(index + direction, 0, options.length - 1)];\n\t\tif (!option || option === options[index] || this.#hasSprungFrom(options[index])) return;\n\n\t\tif (this.#isMomentary(option)) this.#hold(option, pointerId);\n\t\telse this.select(option);\n\t\tthis.#refocus(options);\n\t}\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
+ import { SonicFormElement } from "./form-element.js";
1
2
  import { TimeRegions } from "../lib/time-regions.js";
2
3
  import { ValueMapping } from "../lib/value-mapping.js";
3
- import { SonicFormElement } from "./form-element.js";
4
4
  //#region src/elements/value-element.d.ts
5
5
  export interface ValueAxis {
6
6
  fromProportion?: number;
@@ -49,7 +49,7 @@ export declare abstract class SonicValueElement extends SonicFormElement {
49
49
  get taper(): 'linear' | 'log';
50
50
  set taper(curve: 'linear' | 'log' | undefined);
51
51
  get value(): number;
52
- set value(next: number);
52
+ set value(next: null | number | undefined);
53
53
  get valueText(): string;
54
54
  attributeChangedCallback(name: string): void;
55
55
  connectedCallback(): void;
@@ -77,7 +77,7 @@ export declare abstract class SonicValueElement extends SonicFormElement {
77
77
  protected springTarget(): number | undefined;
78
78
  protected tapTarget(): number | undefined;
79
79
  protected travelPx(control: HTMLElement, property: `--_sonic-${string}`): number;
80
- protected writeModulationValue(control: HTMLElement, prefix: 'dial' | 'slider', next: number | undefined): void;
80
+ protected writeModulationValue(control: HTMLElement, prefix: 'dial' | 'slider', next: null | number | undefined): void;
81
81
  protected writeProportions(control: HTMLElement, prefix: 'dial' | 'slider', modulation: number): void;
82
82
  }
83
83
  //#endregion
@@ -1,15 +1,16 @@
1
- import { clamp } from "../lib/math.js";
1
+ import { clamp, toNumber } from "../lib/math.js";
2
2
  import { placeChildren, requireChild } from "../lib/render.js";
3
3
  import { writeAttribute } from "../lib/write-attribute.js";
4
4
  import { SonicFormElement } from "./form-element.js";
5
+ import { copyNode } from "../lib/copy-node.js";
5
6
  import { ReadoutClaim } from "./readout-claim.js";
6
7
  import { Readout } from "./readout.js";
7
8
  import { ValueEntry } from "./value-entry.js";
8
- import { copyNode } from "../lib/copy-node.js";
9
9
  import { dragThresholdPx, startDrag, stepDrag } from "../lib/drag-step.js";
10
10
  import { focusByPointer } from "../lib/focus-by-pointer.js";
11
11
  import { isMenuPress, isResetPress } from "../lib/modifier-press.js";
12
12
  import { parseNumberList } from "../lib/number-list.js";
13
+ import { bindDrag } from "../lib/pointer-drag.js";
13
14
  import { readPxProperty } from "../lib/read-px-property.js";
14
15
  import { readRegions } from "../lib/time-regions.js";
15
16
  import { resetKeys, valueMapping } from "../lib/value-mapping.js";
@@ -203,53 +204,57 @@ var SonicValueElement = class extends SonicFormElement {
203
204
  return this.#value;
204
205
  }
205
206
  set value(next) {
207
+ if (next === void 0 || next === null) {
208
+ this.formResetCallback();
209
+ return;
210
+ }
211
+ const value = toNumber(next);
206
212
  if (this.#isHeld()) {
207
- this.heldWrite(next);
213
+ this.heldWrite(value);
208
214
  return;
209
215
  }
210
- this.#write(next);
216
+ this.#write(value);
217
+ if (Number.isFinite(value)) this.#asked = value;
211
218
  }
212
219
  get valueText() {
213
220
  return this.#textFor(this.#value);
214
221
  }
222
+ #asked;
215
223
  #buffered = [];
216
224
  #claim = new ReadoutClaim(() => {
217
225
  this.#renderHold();
218
226
  });
219
- #drag;
220
227
  #entry;
221
228
  #escapeWatch;
222
229
  #formatSpokenValue;
223
230
  #formatValue;
224
231
  #instance = String(instanceCount += 1);
225
- #isDirty = false;
226
232
  #isModulated = false;
227
233
  #keyScrub;
228
234
  #land;
229
235
  #limit;
236
+ #mapping;
230
237
  #modulationValue;
231
238
  #parseValue;
239
+ #pointerDrag;
232
240
  #positions;
233
241
  #readout;
234
242
  #scaleMarks;
235
243
  #value = 0;
236
244
  #watchers = /* @__PURE__ */ new Set();
237
245
  attributeChangedCallback(name) {
246
+ this.#mapping = void 0;
238
247
  this.#positions = parseNumberList(this.getAttribute("positions"));
239
248
  if (name === "value") {
240
249
  if (this.#isHeld()) {
241
250
  this.heldWrite(this.numberAttribute("value", this.min));
242
251
  return;
243
252
  }
244
- this.#isDirty = false;
245
- }
246
- if (name === "disabled" && this.isDisabled()) {
247
- this.#entry?.close(false);
248
- this.#endDrag();
249
- this.#endKeyScrub();
253
+ this.#asked = void 0;
250
254
  }
251
- this.#value = this.mapping().snap(this.#isDirty ? this.#value : this.numberAttribute("value", this.min));
252
- if (proportionAttributes.has(name)) this.proportionsChanged();
255
+ if (name === "disabled" && this.isDisabled()) this.#endHolds();
256
+ this.#value = this.mapping().snap(this.#asked ?? this.numberAttribute("value", this.min));
257
+ if (proportionAttributes.has(name) && this.isBound()) this.proportionsChanged();
253
258
  this.render();
254
259
  }
255
260
  connectedCallback() {
@@ -258,66 +263,61 @@ var SonicValueElement = class extends SonicFormElement {
258
263
  }
259
264
  bindGestures(control, signal, grab) {
260
265
  const entry = this.#bindEntry(control, signal);
261
- control.addEventListener("pointerdown", (event) => {
262
- if (!this.#canPress(event, entry)) return;
263
- this.#endDrag();
264
- this.toggleState("springing", false);
265
- if (event.pointerType === "touch") event.preventDefault();
266
- focusByPointer(control);
267
- if (isResetPress(event)) {
268
- this.#reset();
269
- return;
266
+ this.#pointerDrag = bindDrag(control, {
267
+ cancel: (drag) => {
268
+ if (drag.isKeptOnCancel !== true) this.input(drag.fromValue);
269
+ },
270
+ grab: (event) => {
271
+ if (isMenuPress(event) || this.isDisabled() || entry.isOpen) return;
272
+ this.toggleState("springing", false);
273
+ if (event.pointerType === "touch") event.preventDefault();
274
+ focusByPointer(control);
275
+ if (isResetPress(event)) {
276
+ this.#reset();
277
+ return;
278
+ }
279
+ const fromValue = this.#value;
280
+ const axis = grab(event);
281
+ if (!axis) return;
282
+ const drag = {
283
+ ...axis,
284
+ fromValue,
285
+ isOutside: false,
286
+ isRevealed: false,
287
+ revealTimer: void 0,
288
+ state: this.#startDrag(control, axis, event)
289
+ };
290
+ drag.revealTimer = setTimeout(() => {
291
+ this.#reveal(drag);
292
+ }, 250);
293
+ return drag;
294
+ },
295
+ lift: (drag, event) => {
296
+ const tap = drag.state.isEngaged ? void 0 : this.tapTarget();
297
+ if (tap !== void 0) {
298
+ entry.forgetPress();
299
+ this.#commit(tap);
300
+ return;
301
+ }
302
+ if (!entry.press(event) || this.springTarget() !== void 0) return;
303
+ if (this.doublePress === "reset") this.#reset();
304
+ else entry.open();
305
+ },
306
+ move: (drag, event) => {
307
+ this.#dragTo(drag, event);
308
+ },
309
+ release: (drag) => {
310
+ this.#releaseDrag(drag);
311
+ },
312
+ toggle: (isDragging) => {
313
+ this.toggleState("dragging", isDragging);
270
314
  }
271
- const fromValue = this.#value;
272
- const axis = grab(event);
273
- if (!axis) return;
274
- const drag = {
275
- ...axis,
276
- fromValue,
277
- isOutside: false,
278
- isRevealed: false,
279
- pointerId: event.pointerId,
280
- revealTimer: void 0,
281
- state: this.#startDrag(control, axis, event)
282
- };
283
- drag.revealTimer = setTimeout(() => {
284
- this.#reveal(drag);
285
- }, 250);
286
- this.#drag = drag;
287
- control.setPointerCapture(event.pointerId);
288
- this.toggleState("dragging", true);
289
- }, { signal });
290
- control.addEventListener("pointermove", (event) => {
291
- const drag = this.#drag;
292
- if (event.pointerId === drag?.pointerId) this.#dragTo(drag, event);
293
- }, { signal });
294
- const endDrag = (event) => {
295
- if (event.pointerId === this.#drag?.pointerId) this.#endDrag();
296
- };
297
- control.addEventListener("pointercancel", (event) => {
298
- const drag = this.#drag;
299
- if (event.pointerId === drag?.pointerId && drag.isKeptOnCancel !== true) this.input(drag.fromValue);
300
- endDrag(event);
315
+ }, signal);
316
+ control.addEventListener("pointercancel", () => {
301
317
  entry.forgetPress();
302
318
  }, { signal });
303
- control.addEventListener("lostpointercapture", endDrag, { signal });
304
- control.addEventListener("pointerup", (event) => {
305
- const drag = this.#drag;
306
- if (event.pointerId !== drag?.pointerId) return;
307
- this.#endDrag();
308
- const tap = drag.state.isEngaged ? void 0 : this.tapTarget();
309
- if (tap !== void 0) {
310
- entry.forgetPress();
311
- this.#commit(tap);
312
- return;
313
- }
314
- if (!entry.press(event) || this.springTarget() !== void 0) return;
315
- if (this.doublePress === "reset") this.#reset();
316
- else entry.open();
317
- }, { signal });
318
319
  this.#bindKeys(control, entry, signal);
319
320
  signal.addEventListener("abort", () => {
320
- this.#endDrag();
321
321
  this.#endKeyScrub();
322
322
  this.#concealKeyReveal();
323
323
  }, { once: true });
@@ -328,6 +328,7 @@ var SonicValueElement = class extends SonicFormElement {
328
328
  control,
329
329
  copy: scaleMark,
330
330
  isCopied: (child) => child instanceof Element && child.matches("[data-sonic-value]"),
331
+ isPassed: (child) => child instanceof Element && child.matches(".sonic-led"),
331
332
  place: (copies) => {
332
333
  placeChildren(marks, copies);
333
334
  this.#renderScale();
@@ -339,7 +340,7 @@ var SonicValueElement = class extends SonicFormElement {
339
340
  return "slider";
340
341
  }
341
342
  heldFrom() {
342
- return this.#drag?.fromValue ?? this.#keyScrub?.fromValue;
343
+ return this.#dragging()?.fromValue ?? this.#keyScrub?.fromValue;
343
344
  }
344
345
  heldWrite(_next) {}
345
346
  holdChanged() {}
@@ -365,8 +366,9 @@ var SonicValueElement = class extends SonicFormElement {
365
366
  return false;
366
367
  }
367
368
  mapping() {
369
+ if (this.#mapping) return this.#mapping;
368
370
  const { detent, midpoint } = this;
369
- return valueMapping({
371
+ this.#mapping = valueMapping({
370
372
  ...detent === void 0 ? {} : { detent },
371
373
  ...this.#positions ? { positions: this.#positions } : {},
372
374
  max: this.max,
@@ -377,6 +379,7 @@ var SonicValueElement = class extends SonicFormElement {
377
379
  step: this.step,
378
380
  taper: this.taper
379
381
  });
382
+ return this.#mapping;
380
383
  }
381
384
  originValue() {
382
385
  return this.origin ?? this.mapping().bounds[0];
@@ -389,10 +392,7 @@ var SonicValueElement = class extends SonicFormElement {
389
392
  return this.#claim.shown(this.#value, this.readout).value ?? this.#value;
390
393
  }
391
394
  render() {
392
- this.draw();
393
- this.#renderScale();
394
- this.toggleState("at-origin", !this.isWrapping() && this.#value === this.mapping().snap(this.originValue()));
395
- this.#renderAria(this.focusTarget(), this.controlOrientation());
395
+ if (this.isBound()) this.#renderControl();
396
396
  for (const listener of this.#watchers) listener();
397
397
  }
398
398
  restoreState(state) {
@@ -407,9 +407,10 @@ var SonicValueElement = class extends SonicFormElement {
407
407
  return Math.max(1, readPxProperty(getComputedStyle(control), property, fallbackTravelPx));
408
408
  }
409
409
  writeModulationValue(control, prefix, next) {
410
- if (next !== void 0 && !Number.isFinite(next)) return;
411
- this.#modulationValue = next;
412
- this.#writeModulationProportion(control, prefix);
410
+ const value = next === void 0 || next === null ? void 0 : toNumber(next);
411
+ if (value !== void 0 && !Number.isFinite(value)) return;
412
+ this.#modulationValue = value;
413
+ if (this.isBound()) this.#writeModulationProportion(control, prefix);
413
414
  }
414
415
  writeProportions(control, prefix, modulation) {
415
416
  const { style } = control;
@@ -469,7 +470,7 @@ var SonicValueElement = class extends SonicFormElement {
469
470
  control.addEventListener("keyup", (event) => {
470
471
  const isMeta = event.key === "Meta";
471
472
  if (isMeta || event.key === this.#keyScrub?.key) this.#endKeyScrub();
472
- if (isMeta && this.#drag) return;
473
+ if (isMeta && this.#dragging()) return;
473
474
  if (isMeta || this.mapping().keyTarget(event.key, this.#value) !== void 0) this.#springBack();
474
475
  }, { signal });
475
476
  control.addEventListener("blur", () => {
@@ -478,10 +479,6 @@ var SonicValueElement = class extends SonicFormElement {
478
479
  this.#springBack();
479
480
  }, { signal });
480
481
  }
481
- #canPress(event, entry) {
482
- if (event.button !== 0 || isMenuPress(event) || this.isDisabled() || entry.isOpen) return false;
483
- return !this.#drag || this.#drag.pointerId === event.pointerId;
484
- }
485
482
  #commit(next, direction = 0) {
486
483
  const landed = Number.isFinite(next) ? this.#land?.(next, direction) ?? next : next;
487
484
  if (this.input(landed)) this.dispatchEvent(new Event("change", { bubbles: true }));
@@ -489,6 +486,9 @@ var SonicValueElement = class extends SonicFormElement {
489
486
  #concealKeyReveal() {
490
487
  if (this.#claim.conceal("keys")) this.#renderHold();
491
488
  }
489
+ #dragging() {
490
+ return this.#pointerDrag?.current();
491
+ }
492
492
  #dragTo(drag, event) {
493
493
  const isOutside = drag.outside?.(event) === true;
494
494
  const wasEngaged = drag.state.isEngaged;
@@ -506,16 +506,10 @@ var SonicValueElement = class extends SonicFormElement {
506
506
  drag.isOutside = isOutside;
507
507
  this.#renderDragReveal(drag);
508
508
  }
509
- #endDrag() {
510
- const drag = this.#drag;
511
- if (!drag) return;
512
- clearTimeout(drag.revealTimer);
513
- this.#drag = void 0;
514
- this.toggleState("dragging", false);
515
- this.#claim.conceal("drag");
516
- this.#renderHold();
517
- if (this.#value !== drag.fromValue) this.dispatchEvent(new Event("change", { bubbles: true }));
518
- this.#springBack();
509
+ #endHolds() {
510
+ this.#entry?.close(false);
511
+ this.#pointerDrag?.end();
512
+ this.#endKeyScrub();
519
513
  }
520
514
  #endKeyScrub() {
521
515
  const scrub = this.#keyScrub;
@@ -525,7 +519,7 @@ var SonicValueElement = class extends SonicFormElement {
525
519
  if (this.#value !== scrub.fromValue) this.dispatchEvent(new Event("change", { bubbles: true }));
526
520
  }
527
521
  #isHeld() {
528
- return this.#drag !== void 0 || this.#keyScrub !== void 0;
522
+ return this.#dragging() !== void 0 || this.#keyScrub !== void 0;
529
523
  }
530
524
  #keyTarget(key) {
531
525
  if (resetKeys.has(key)) return this.default;
@@ -539,6 +533,13 @@ var SonicValueElement = class extends SonicFormElement {
539
533
  this.#endKeyScrub();
540
534
  this.#commit(next, next > this.#value ? 1 : -1);
541
535
  }
536
+ #releaseDrag(drag) {
537
+ clearTimeout(drag.revealTimer);
538
+ this.#claim.conceal("drag");
539
+ this.#renderHold();
540
+ if (this.#value !== drag.fromValue) this.dispatchEvent(new Event("change", { bubbles: true }));
541
+ this.#springBack();
542
+ }
542
543
  #renderAria(control, orientation) {
543
544
  this.writeFormValue(String(this.#value), String(this.#value));
544
545
  if (this.#entry?.isOpen) {
@@ -548,24 +549,26 @@ var SonicValueElement = class extends SonicFormElement {
548
549
  return;
549
550
  }
550
551
  const [low, high] = this.mapping().bounds;
551
- control.setAttribute("role", this.controlRole());
552
- control.setAttribute("aria-valuemin", String(low));
553
- control.setAttribute("aria-valuemax", String(high));
554
- control.setAttribute("aria-valuenow", String(this.#value));
555
- if (orientation) control.setAttribute("aria-orientation", orientation);
552
+ writeAttribute(control, "role", this.controlRole());
553
+ writeAttribute(control, "aria-valuemin", String(low));
554
+ writeAttribute(control, "aria-valuemax", String(high));
555
+ writeAttribute(control, "aria-valuenow", String(this.#value));
556
+ if (orientation) writeAttribute(control, "aria-orientation", orientation);
556
557
  writeAttribute(control, "aria-valuetext", (this.#formatSpokenValue ?? this.#formatValue)?.(this.#value));
557
558
  this.forwardNaming(control, true);
558
559
  this.#renderDisabled(control);
559
560
  this.#renderReadout();
560
561
  }
562
+ #renderControl() {
563
+ this.draw();
564
+ this.#renderScale();
565
+ this.toggleState("at-origin", !this.isWrapping() && this.#value === this.mapping().snap(this.originValue()));
566
+ this.#renderAria(this.focusTarget(), this.controlOrientation());
567
+ }
561
568
  #renderDisabled(control) {
562
- if (this.isDisabled()) {
563
- control.setAttribute("aria-disabled", "true");
564
- control.removeAttribute("tabindex");
565
- return;
566
- }
567
- control.removeAttribute("aria-disabled");
568
- control.setAttribute("tabindex", "0");
569
+ const isDisabled = this.isDisabled();
570
+ writeAttribute(control, "aria-disabled", isDisabled ? "true" : void 0);
571
+ writeAttribute(control, "tabindex", isDisabled ? void 0 : "0");
569
572
  }
570
573
  #renderDragReveal(drag) {
571
574
  if (drag.isRevealed && !drag.isOutside) this.#claim.reveal("drag");
@@ -599,7 +602,7 @@ var SonicValueElement = class extends SonicFormElement {
599
602
  this.#commit(this.default ?? NaN);
600
603
  }
601
604
  #reveal(drag) {
602
- if (drag.isRevealed || this.#drag !== drag) return;
605
+ if (drag.isRevealed || this.#dragging() !== drag) return;
603
606
  drag.isRevealed = true;
604
607
  this.#renderDragReveal(drag);
605
608
  }
@@ -666,8 +669,8 @@ var SonicValueElement = class extends SonicFormElement {
666
669
  }
667
670
  #write(next) {
668
671
  if (!Number.isFinite(next)) return;
669
- this.#isDirty = true;
670
672
  const clamped = this.mapping().snap(next);
673
+ this.#asked = clamped;
671
674
  if (clamped === this.#value) return;
672
675
  this.#value = clamped;
673
676
  this.render();