@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,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();
@@ -8,7 +8,7 @@ var SonicWaveElement = class extends SonicValueElement {
8
8
  }
9
9
  set markers(list) {
10
10
  this.#markers = [...list ?? []].filter((marker) => Number.isFinite(marker.start)).map((marker) => ({ ...marker })).toSorted((first, second) => first.start - second.start);
11
- this.renderMarkers();
11
+ if (this.isBound()) this.renderMarkers();
12
12
  }
13
13
  #heldAt;
14
14
  #markers = [];
@@ -22,7 +22,7 @@ var SonicWaveElement = class extends SonicValueElement {
22
22
  }
23
23
  connect(signal) {
24
24
  const control = this.control;
25
- this.appendOnce(control);
25
+ this.keepControl(control, signal);
26
26
  this.renderEmpty();
27
27
  this.#surface = bindSurface({
28
28
  canvas: this.canvas,
@@ -50,6 +50,9 @@ var SonicWaveElement = class extends SonicValueElement {
50
50
  if (this.heldFrom() === void 0) this.#heldAt = void 0;
51
51
  this.render();
52
52
  }
53
+ markerList() {
54
+ return this.#markers;
55
+ }
53
56
  markerProportions() {
54
57
  const mapping = this.mapping();
55
58
  return this.#markers.map(({ end, start }) => {
@@ -3,6 +3,7 @@ import { requireChild, template } from "../lib/render.js";
3
3
  import { readRegions } from "../lib/time-regions.js";
4
4
  import { SonicWaveElement } from "./wave-element.js";
5
5
  import { frameTimeline } from "../lib/frame-timeline.js";
6
+ import { isKind, writeKind } from "../lib/marker-band.js";
6
7
  import { createLabelRider } from "../lib/marker-rider.js";
7
8
  import { createTrackingClock } from "../lib/tracking-clock.js";
8
9
  import { waveformBuckets } from "../lib/waveform-buckets.js";
@@ -172,6 +173,7 @@ var SonicWaveform = class extends SonicWaveElement {
172
173
  #drawn;
173
174
  #ghost = requireChild(this.control, ".sonic-waveform-ghost", HTMLDivElement);
174
175
  #grabbed;
176
+ #kindColours = /* @__PURE__ */ new Map();
175
177
  #paintedKey = "";
176
178
  #peaks;
177
179
  #pending = [];
@@ -180,6 +182,7 @@ var SonicWaveform = class extends SonicWaveElement {
180
182
  #requestPeaks;
181
183
  #rider = createLabelRider(this.control, {
182
184
  className: "sonic-waveform-label",
185
+ colourProperty: "--_sonic-waveform-marker",
183
186
  insetProperty: "--_sonic-waveform-label-inset"
184
187
  });
185
188
  connectedCallback() {
@@ -199,7 +202,7 @@ var SonicWaveform = class extends SonicWaveElement {
199
202
  isEmpty() {
200
203
  return !this.#peaks || this.#peaks.samples.length === 0;
201
204
  }
202
- paint(context, { frameMs, isDirty, look, size }) {
205
+ paint(context, { frameMs, isDirty, isRebuilt, look, size }) {
203
206
  const held = this.#held();
204
207
  const timeline = frameTimeline({
205
208
  clockSeconds: this.#clock.read(frameMs, {
@@ -235,19 +238,20 @@ var SonicWaveform = class extends SonicWaveElement {
235
238
  this.#paintScene(context, {
236
239
  ...view,
237
240
  colours: look.colours
238
- });
241
+ }, isRebuilt);
239
242
  if (timeline.wanted) this.#requestPeaks?.(...timeline.wanted);
240
243
  }
241
244
  if (timeline.isMoving) this.surface()?.requestFrame();
242
245
  }
243
246
  renderMarkers() {
244
247
  const bounds = this.mapping().bounds;
245
- this.#rider.measure(this.markers.flatMap(({ dimmed, label, start }) => label === void 0 || label === "" ? [] : [{
248
+ this.#rider.measure(this.markers.flatMap(({ dimmed, kind, label, start }) => label === void 0 || label === "" ? [] : [{
246
249
  isDimmed: dimmed === true,
250
+ ...kind === void 0 ? {} : { kind },
247
251
  start: clamp(start, ...bounds),
248
252
  text: label
249
253
  }]));
250
- this.surface()?.invalidate();
254
+ this.surface()?.rebuild();
251
255
  }
252
256
  #grab() {
253
257
  const drawn = this.#drawn ?? {
@@ -273,10 +277,21 @@ var SonicWaveform = class extends SonicWaveElement {
273
277
  seconds: this.value === from ? grabbed.seconds : this.value
274
278
  };
275
279
  }
276
- #paintScene(context, scene) {
280
+ #paintMarkers(context, scene, isRebuilt) {
281
+ if (isRebuilt) this.#kindColours = this.#readKindColours();
282
+ const line = Math.max(1, Math.round(scene.dpr));
283
+ for (const { dimmed, kind, start } of this.markers) {
284
+ const x = (start - scene.startSeconds) * scene.pixelsPerSecond;
285
+ context.fillStyle = (isKind(kind) ? this.#kindColours.get(kind) : void 0) ?? scene.colours.marker;
286
+ context.globalAlpha = dimmed === true ? .5 : 1;
287
+ context.fillRect(x - line / 2, 0, line, scene.height);
288
+ }
289
+ context.globalAlpha = 1;
290
+ }
291
+ #paintScene(context, scene, isRebuilt) {
277
292
  const { dpr, height, pixelsPerSecond, startSeconds, width } = scene;
278
293
  const x = (seconds) => (seconds - startSeconds) * pixelsPerSecond;
279
- const { grid, marker } = scene.colours;
294
+ const { grid } = scene.colours;
280
295
  const line = Math.max(1, Math.round(dpr));
281
296
  const [opening, closing] = [x(this.min), x(Math.max(this.min, this.max))];
282
297
  const [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];
@@ -289,12 +304,20 @@ var SonicWaveform = class extends SonicWaveElement {
289
304
  if (this.#peaks) paintWave(context, scene, this.#peaks);
290
305
  context.fillStyle = grid;
291
306
  for (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);
292
- context.fillStyle = marker;
293
- for (const marker of this.markers) {
294
- context.globalAlpha = marker.dimmed === true ? .5 : 1;
295
- context.fillRect(x(marker.start) - line / 2, 0, line, height);
296
- }
297
- context.globalAlpha = 1;
307
+ this.#paintMarkers(context, scene, isRebuilt);
308
+ }
309
+ #readKindColours() {
310
+ const kinds = [...new Set(this.markers.map(({ kind }) => kind).filter((kind) => isKind(kind)))];
311
+ const probes = kinds.map((kind) => {
312
+ const probe = document.createElement("div");
313
+ probe.className = "sonic-waveform-label";
314
+ writeKind(probe, kind, "--_sonic-waveform-marker");
315
+ return probe;
316
+ });
317
+ this.control.append(...probes);
318
+ const read = probes.map((probe) => getComputedStyle(probe).color);
319
+ for (const probe of probes) probe.remove();
320
+ return new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
298
321
  }
299
322
  #sourceSeconds() {
300
323
  return this.#readTime?.() ?? this.playback();