@xsynaptic/sonic-ui 0.5.0 → 0.6.1

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 (138) 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 +6 -5
  6. package/dist/default/elements/dial.js +5 -1
  7. package/dist/default/elements/envelope.js +46 -18
  8. package/dist/default/elements/meter.js +12 -8
  9. package/dist/default/elements/number.js +1 -1
  10. package/dist/default/elements/radio-group.js +46 -47
  11. package/dist/default/elements/readout-claim.js +0 -29
  12. package/dist/default/elements/readout.js +1 -1
  13. package/dist/default/elements/scale.js +43 -0
  14. package/dist/default/elements/slider.js +16 -3
  15. package/dist/default/elements/sonic-element.js +17 -55
  16. package/dist/default/elements/switch.js +33 -39
  17. package/dist/default/elements/value-element.js +142 -187
  18. package/dist/default/elements/wave-element.js +5 -2
  19. package/dist/default/elements/waveform.js +35 -12
  20. package/dist/default/elements/wavestrip.js +59 -18
  21. package/dist/default/elements/xy.js +34 -19
  22. package/dist/default/index.js +2 -2
  23. package/dist/default/lib/adsr-shape.js +5 -0
  24. package/dist/default/lib/ballistics.js +2 -2
  25. package/dist/default/lib/canvas-surface.js +13 -2
  26. package/dist/default/lib/distribute.js +1 -1
  27. package/dist/default/lib/drag-step.js +4 -4
  28. package/dist/default/lib/frame-timeline.js +2 -1
  29. package/dist/default/lib/marker-band.js +54 -10
  30. package/dist/default/lib/marker-rider.js +10 -4
  31. package/dist/default/lib/math.js +6 -1
  32. package/dist/default/lib/mirror-children.js +64 -0
  33. package/dist/default/lib/owned-control.js +10 -0
  34. package/dist/default/lib/pointer-drag.js +8 -5
  35. package/dist/default/lib/resample-peaks.js +11 -1
  36. package/dist/default/lib/time-regions.js +4 -1
  37. package/dist/default/lib/write-attribute.js +1 -0
  38. package/dist/dev/dat.d.ts +2 -2
  39. package/dist/dev/dat.js +5 -4
  40. package/dist/dev/dat.js.map +1 -1
  41. package/dist/dev/define/button.js +1 -1
  42. package/dist/dev/define.d.ts +4 -4
  43. package/dist/dev/define.js +2 -2
  44. package/dist/dev/elements/button.js +6 -5
  45. package/dist/dev/elements/button.js.map +1 -1
  46. package/dist/dev/elements/dial.d.ts +1 -1
  47. package/dist/dev/elements/dial.js +5 -1
  48. package/dist/dev/elements/dial.js.map +1 -1
  49. package/dist/dev/elements/envelope.js +46 -18
  50. package/dist/dev/elements/envelope.js.map +1 -1
  51. package/dist/dev/elements/meter.d.ts +1 -1
  52. package/dist/dev/elements/meter.js +12 -8
  53. package/dist/dev/elements/meter.js.map +1 -1
  54. package/dist/dev/elements/number.js +1 -1
  55. package/dist/dev/elements/number.js.map +1 -1
  56. package/dist/dev/elements/radio-group.js +46 -47
  57. package/dist/dev/elements/radio-group.js.map +1 -1
  58. package/dist/dev/elements/readout-claim.js +0 -29
  59. package/dist/dev/elements/readout-claim.js.map +1 -1
  60. package/dist/dev/elements/readout.js +1 -1
  61. package/dist/dev/elements/readout.js.map +1 -1
  62. package/dist/dev/elements/scale.js +44 -0
  63. package/dist/dev/elements/scale.js.map +1 -0
  64. package/dist/dev/elements/slider.d.ts +4 -1
  65. package/dist/dev/elements/slider.js +16 -3
  66. package/dist/dev/elements/slider.js.map +1 -1
  67. package/dist/dev/elements/sonic-element.d.ts +2 -8
  68. package/dist/dev/elements/sonic-element.js +18 -55
  69. package/dist/dev/elements/sonic-element.js.map +1 -1
  70. package/dist/dev/elements/switch.js +33 -39
  71. package/dist/dev/elements/switch.js.map +1 -1
  72. package/dist/dev/elements/value-element.d.ts +6 -8
  73. package/dist/dev/elements/value-element.js +142 -187
  74. package/dist/dev/elements/value-element.js.map +1 -1
  75. package/dist/dev/elements/wave-element.d.ts +1 -0
  76. package/dist/dev/elements/wave-element.js +5 -2
  77. package/dist/dev/elements/wave-element.js.map +1 -1
  78. package/dist/dev/elements/waveform.d.ts +1 -1
  79. package/dist/dev/elements/waveform.js +35 -12
  80. package/dist/dev/elements/waveform.js.map +1 -1
  81. package/dist/dev/elements/wavestrip.d.ts +4 -0
  82. package/dist/dev/elements/wavestrip.js +59 -18
  83. package/dist/dev/elements/wavestrip.js.map +1 -1
  84. package/dist/dev/elements/xy.d.ts +3 -3
  85. package/dist/dev/elements/xy.js +34 -19
  86. package/dist/dev/elements/xy.js.map +1 -1
  87. package/dist/dev/index.d.ts +2 -2
  88. package/dist/dev/index.js +2 -2
  89. package/dist/dev/lib/adsr-shape.js +5 -0
  90. package/dist/dev/lib/adsr-shape.js.map +1 -1
  91. package/dist/dev/lib/ballistics.js +2 -2
  92. package/dist/dev/lib/ballistics.js.map +1 -1
  93. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  94. package/dist/dev/lib/canvas-surface.js +13 -2
  95. package/dist/dev/lib/canvas-surface.js.map +1 -1
  96. package/dist/dev/lib/distribute.js +1 -1
  97. package/dist/dev/lib/distribute.js.map +1 -1
  98. package/dist/dev/lib/drag-step.js +4 -4
  99. package/dist/dev/lib/drag-step.js.map +1 -1
  100. package/dist/dev/lib/frame-timeline.js +2 -1
  101. package/dist/dev/lib/frame-timeline.js.map +1 -1
  102. package/dist/dev/lib/marker-band.js +54 -10
  103. package/dist/dev/lib/marker-band.js.map +1 -1
  104. package/dist/dev/lib/marker-rider.js +10 -4
  105. package/dist/dev/lib/marker-rider.js.map +1 -1
  106. package/dist/dev/lib/math.js +6 -1
  107. package/dist/dev/lib/math.js.map +1 -1
  108. package/dist/dev/lib/mirror-children.js +80 -0
  109. package/dist/dev/lib/mirror-children.js.map +1 -0
  110. package/dist/dev/lib/owned-control.js +23 -0
  111. package/dist/dev/lib/owned-control.js.map +1 -0
  112. package/dist/dev/lib/pointer-drag.js +8 -5
  113. package/dist/dev/lib/pointer-drag.js.map +1 -1
  114. package/dist/dev/lib/resample-peaks.js +11 -1
  115. package/dist/dev/lib/resample-peaks.js.map +1 -1
  116. package/dist/dev/lib/time-regions.js +4 -1
  117. package/dist/dev/lib/time-regions.js.map +1 -1
  118. package/dist/dev/lib/write-attribute.js +1 -0
  119. package/dist/dev/lib/write-attribute.js.map +1 -1
  120. package/dist/skins/amber.css +8 -0
  121. package/dist/skins/flat.css +26 -0
  122. package/dist/skins/ivory.css +1 -0
  123. package/dist/skins/lime.css +2 -0
  124. package/dist/skins/slate.css +2 -0
  125. package/dist/styles/button.css +59 -10
  126. package/dist/styles/dial.css +24 -16
  127. package/dist/styles/envelope.css +58 -13
  128. package/dist/styles/led.css +20 -4
  129. package/dist/styles/material.css +143 -55
  130. package/dist/styles/meter.css +8 -4
  131. package/dist/styles/panel.css +7 -4
  132. package/dist/styles/segmented.css +27 -5
  133. package/dist/styles/slider.css +2 -1
  134. package/dist/styles/switch.css +65 -23
  135. package/dist/styles/waveform.css +28 -5
  136. package/dist/styles/wavestrip.css +23 -6
  137. package/dist/styles/xy.css +48 -27
  138. package/package.json +2 -5
@@ -1,17 +1,16 @@
1
- import { clamp } from "../lib/math.js";
2
- import { placeChildren, requireChild } from "../lib/render.js";
1
+ import { clamp, toNumber } from "../lib/math.js";
3
2
  import { writeAttribute } from "../lib/write-attribute.js";
4
3
  import { SonicFormElement } from "./form-element.js";
4
+ import { requireChild } from "../lib/render.js";
5
5
  import { ReadoutClaim } from "./readout-claim.js";
6
6
  import { Readout } from "./readout.js";
7
7
  import { ValueEntry } from "./value-entry.js";
8
- import { copyNode } from "../lib/copy-node.js";
9
8
  import { dragThresholdPx, startDrag, stepDrag } from "../lib/drag-step.js";
10
9
  import { focusByPointer } from "../lib/focus-by-pointer.js";
11
10
  import { isMenuPress, isResetPress } from "../lib/modifier-press.js";
12
11
  import { parseNumberList } from "../lib/number-list.js";
12
+ import { bindDrag } from "../lib/pointer-drag.js";
13
13
  import { readPxProperty } from "../lib/read-px-property.js";
14
- import { readRegions } from "../lib/time-regions.js";
15
14
  import { resetKeys, valueMapping } from "../lib/value-mapping.js";
16
15
 
17
16
  //#region src/elements/value-element.ts
@@ -36,18 +35,6 @@ const roleAttributes = [
36
35
  "role",
37
36
  "tabindex"
38
37
  ];
39
- function scaleValue(mark) {
40
- const text = mark.dataset.sonicValue?.trim();
41
- return text ? Number(text) : NaN;
42
- }
43
- function scaleMark(original) {
44
- const mark = copyNode(original);
45
- if (!(mark instanceof HTMLElement || mark instanceof SVGElement)) return void 0;
46
- if (!Number.isFinite(scaleValue(mark))) return void 0;
47
- const isTick = mark.childElementCount === 0 && mark.textContent.trim() === "";
48
- mark.classList.add(isTick ? "sonic-scale-tick" : "sonic-scale-label");
49
- return mark;
50
- }
51
38
  let link;
52
39
  function linkValue(element) {
53
40
  return link(element);
@@ -63,6 +50,7 @@ var SonicValueElement = class extends SonicFormElement {
63
50
  "origin",
64
51
  "readout",
65
52
  "step",
53
+ "tabindex",
66
54
  "taper",
67
55
  "value",
68
56
  "positions"
@@ -88,13 +76,6 @@ var SonicValueElement = class extends SonicFormElement {
88
76
  }
89
77
  });
90
78
  }
91
- get buffered() {
92
- return this.#buffered.map(([start, end]) => [start, end]);
93
- }
94
- set buffered(regions) {
95
- this.#buffered = regions ? readRegions(regions).toSorted((first, second) => first[0] - second[0]) : [];
96
- this.render();
97
- }
98
79
  get default() {
99
80
  return this.optionalNumberAttribute("default");
100
81
  }
@@ -203,151 +184,130 @@ var SonicValueElement = class extends SonicFormElement {
203
184
  return this.#value;
204
185
  }
205
186
  set value(next) {
187
+ if (next === void 0 || next === null) {
188
+ this.formResetCallback();
189
+ return;
190
+ }
191
+ const value = toNumber(next);
206
192
  if (this.#isHeld()) {
207
- this.heldWrite(next);
193
+ this.heldWrite(value);
208
194
  return;
209
195
  }
210
- this.#write(next);
196
+ this.#write(value);
197
+ if (Number.isFinite(value)) this.#asked = value;
211
198
  }
212
199
  get valueText() {
213
200
  return this.#textFor(this.#value);
214
201
  }
215
- #buffered = [];
202
+ #asked;
216
203
  #claim = new ReadoutClaim(() => {
217
204
  this.#renderHold();
218
205
  });
219
- #drag;
220
206
  #entry;
221
- #escapeWatch;
222
207
  #formatSpokenValue;
223
208
  #formatValue;
224
209
  #instance = String(instanceCount += 1);
225
- #isDirty = false;
226
210
  #isModulated = false;
227
211
  #keyScrub;
228
212
  #land;
229
213
  #limit;
214
+ #mapping;
230
215
  #modulationValue;
231
216
  #parseValue;
217
+ #pointerDrag;
232
218
  #positions;
233
219
  #readout;
234
- #scaleMarks;
235
220
  #value = 0;
236
221
  #watchers = /* @__PURE__ */ new Set();
237
222
  attributeChangedCallback(name) {
223
+ this.#mapping = void 0;
238
224
  this.#positions = parseNumberList(this.getAttribute("positions"));
239
225
  if (name === "value") {
240
226
  if (this.#isHeld()) {
241
227
  this.heldWrite(this.numberAttribute("value", this.min));
242
228
  return;
243
229
  }
244
- this.#isDirty = false;
230
+ this.#asked = void 0;
245
231
  }
246
- if (name === "disabled" && this.isDisabled()) {
247
- this.#entry?.close(false);
248
- this.#endDrag();
249
- this.#endKeyScrub();
250
- }
251
- this.#value = this.mapping().snap(this.#isDirty ? this.#value : this.numberAttribute("value", this.min));
252
- if (proportionAttributes.has(name)) this.proportionsChanged();
232
+ if (name === "disabled" && this.isDisabled()) this.#endHolds();
233
+ this.#value = this.mapping().snap(this.#asked ?? this.numberAttribute("value", this.min));
234
+ if (proportionAttributes.has(name) && this.isBound()) this.proportionsChanged();
253
235
  this.render();
254
236
  }
255
237
  connectedCallback() {
256
- this.upgradeProperties("buffered", "default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "step", "taper", "positions", "detent", "formatValue", "formatSpokenValue", "keyStep", "parseValue", "value");
238
+ this.upgradeProperties("default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "step", "taper", "positions", "detent", "formatValue", "formatSpokenValue", "keyStep", "parseValue", "value");
257
239
  super.connectedCallback();
258
240
  }
259
241
  bindGestures(control, signal, grab) {
260
242
  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;
243
+ this.#pointerDrag = bindDrag(control, {
244
+ cancel: (drag) => {
245
+ if (drag.isKeptOnCancel !== true) this.input(drag.fromValue);
246
+ },
247
+ grab: (event) => {
248
+ if (isMenuPress(event) || this.isDisabled() || entry.isOpen) return;
249
+ this.toggleState("springing", false);
250
+ if (event.pointerType === "touch") event.preventDefault();
251
+ focusByPointer(control);
252
+ if (isResetPress(event)) {
253
+ this.#reset();
254
+ return;
255
+ }
256
+ const fromValue = this.#value;
257
+ const axis = grab(event);
258
+ if (!axis) return;
259
+ const drag = {
260
+ ...axis,
261
+ fromValue,
262
+ isOutside: false,
263
+ isRevealed: false,
264
+ revealTimer: void 0,
265
+ state: this.#startDrag(control, axis, event)
266
+ };
267
+ drag.revealTimer = setTimeout(() => {
268
+ this.#reveal(drag);
269
+ }, 250);
270
+ return drag;
271
+ },
272
+ lift: (drag, event) => {
273
+ const tap = drag.state.isEngaged ? void 0 : this.tapTarget();
274
+ if (tap !== void 0) {
275
+ entry.forgetPress();
276
+ this.#commit(tap);
277
+ return;
278
+ }
279
+ if (!entry.press(event) || this.springTarget() !== void 0) return;
280
+ if (this.doublePress === "reset") this.#reset();
281
+ else entry.open();
282
+ },
283
+ move: (drag, event) => {
284
+ this.#dragTo(drag, event);
285
+ },
286
+ release: (drag) => {
287
+ this.#releaseDrag(drag);
288
+ },
289
+ toggle: (isDragging) => {
290
+ this.toggleState("dragging", isDragging);
270
291
  }
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);
292
+ }, signal);
293
+ control.addEventListener("pointercancel", () => {
301
294
  entry.forgetPress();
302
295
  }, { 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
296
  this.#bindKeys(control, entry, signal);
319
297
  signal.addEventListener("abort", () => {
320
- this.#endDrag();
321
298
  this.#endKeyScrub();
322
299
  this.#concealKeyReveal();
323
300
  }, { once: true });
324
301
  }
325
- bindScale(control, marks, signal) {
326
- this.#scaleMarks = marks;
327
- this.mirrorChildren({
328
- control,
329
- copy: scaleMark,
330
- isCopied: (child) => child instanceof Element && child.matches("[data-sonic-value]"),
331
- place: (copies) => {
332
- placeChildren(marks, copies);
333
- this.#renderScale();
334
- }
335
- }, signal);
336
- }
337
302
  controlOrientation() {}
338
303
  controlRole() {
339
304
  return "slider";
340
305
  }
341
306
  heldFrom() {
342
- return this.#drag?.fromValue ?? this.#keyScrub?.fromValue;
307
+ return this.#dragging()?.fromValue ?? this.#keyScrub?.fromValue;
343
308
  }
344
309
  heldWrite(_next) {}
345
310
  holdChanged() {}
346
- hoverReadout(value) {
347
- this.#claim.hover(value);
348
- this.#renderReadout();
349
- this.#watchEscape(value !== void 0);
350
- }
351
311
  input(next) {
352
312
  const previous = this.#value;
353
313
  this.#write(this.#limit ? clamp(next, ...this.#limit) : next);
@@ -358,6 +318,9 @@ var SonicValueElement = class extends SonicFormElement {
358
318
  }));
359
319
  return true;
360
320
  }
321
+ isEditing() {
322
+ return this.#entry?.isOpen === true;
323
+ }
361
324
  isRevealed() {
362
325
  return this.#claim.isRevealed;
363
326
  }
@@ -365,8 +328,9 @@ var SonicValueElement = class extends SonicFormElement {
365
328
  return false;
366
329
  }
367
330
  mapping() {
331
+ if (this.#mapping) return this.#mapping;
368
332
  const { detent, midpoint } = this;
369
- return valueMapping({
333
+ this.#mapping = valueMapping({
370
334
  ...detent === void 0 ? {} : { detent },
371
335
  ...this.#positions ? { positions: this.#positions } : {},
372
336
  max: this.max,
@@ -377,24 +341,33 @@ var SonicValueElement = class extends SonicFormElement {
377
341
  step: this.step,
378
342
  taper: this.taper
379
343
  });
344
+ return this.#mapping;
380
345
  }
381
346
  originValue() {
382
347
  return this.origin ?? this.mapping().bounds[0];
383
348
  }
349
+ previewValue() {}
384
350
  proportionsChanged() {}
385
351
  readModulationValue() {
386
352
  return this.#modulationValue;
387
353
  }
388
354
  readoutValue() {
389
- return this.#claim.shown(this.#value, this.readout).value ?? this.#value;
355
+ return this.#shown().value ?? this.#value;
390
356
  }
391
357
  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());
358
+ if (this.isBound()) this.#renderControl();
396
359
  for (const listener of this.#watchers) listener();
397
360
  }
361
+ renderReadout() {
362
+ const readout = this.#readout;
363
+ if (!readout) return;
364
+ const { isOpen, value } = this.#shown();
365
+ readout.bubble.show({
366
+ anchor: readout.anchor,
367
+ isOpen,
368
+ text: value === void 0 ? void 0 : this.#textFor(value)
369
+ });
370
+ }
398
371
  restoreState(state) {
399
372
  this.value = Number(state);
400
373
  }
@@ -407,9 +380,10 @@ var SonicValueElement = class extends SonicFormElement {
407
380
  return Math.max(1, readPxProperty(getComputedStyle(control), property, fallbackTravelPx));
408
381
  }
409
382
  writeModulationValue(control, prefix, next) {
410
- if (next !== void 0 && !Number.isFinite(next)) return;
411
- this.#modulationValue = next;
412
- this.#writeModulationProportion(control, prefix);
383
+ const value = next === void 0 || next === null ? void 0 : toNumber(next);
384
+ if (value !== void 0 && !Number.isFinite(value)) return;
385
+ this.#modulationValue = value;
386
+ if (this.isBound()) this.#writeModulationProportion(control, prefix);
413
387
  }
414
388
  writeProportions(control, prefix, modulation) {
415
389
  const { style } = control;
@@ -439,7 +413,6 @@ var SonicValueElement = class extends SonicFormElement {
439
413
  },
440
414
  text: () => this.#parseValue ? this.valueText : String(this.#value),
441
415
  toggle: (isOpen) => {
442
- this.#claim.edit(isOpen);
443
416
  if (isOpen) this.forwardNaming(input, true);
444
417
  this.toggleState("editing", isOpen);
445
418
  this.render();
@@ -469,7 +442,7 @@ var SonicValueElement = class extends SonicFormElement {
469
442
  control.addEventListener("keyup", (event) => {
470
443
  const isMeta = event.key === "Meta";
471
444
  if (isMeta || event.key === this.#keyScrub?.key) this.#endKeyScrub();
472
- if (isMeta && this.#drag) return;
445
+ if (isMeta && this.#dragging()) return;
473
446
  if (isMeta || this.mapping().keyTarget(event.key, this.#value) !== void 0) this.#springBack();
474
447
  }, { signal });
475
448
  control.addEventListener("blur", () => {
@@ -478,10 +451,6 @@ var SonicValueElement = class extends SonicFormElement {
478
451
  this.#springBack();
479
452
  }, { signal });
480
453
  }
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
454
  #commit(next, direction = 0) {
486
455
  const landed = Number.isFinite(next) ? this.#land?.(next, direction) ?? next : next;
487
456
  if (this.input(landed)) this.dispatchEvent(new Event("change", { bubbles: true }));
@@ -489,6 +458,9 @@ var SonicValueElement = class extends SonicFormElement {
489
458
  #concealKeyReveal() {
490
459
  if (this.#claim.conceal("keys")) this.#renderHold();
491
460
  }
461
+ #dragging() {
462
+ return this.#pointerDrag?.current();
463
+ }
492
464
  #dragTo(drag, event) {
493
465
  const isOutside = drag.outside?.(event) === true;
494
466
  const wasEngaged = drag.state.isEngaged;
@@ -506,16 +478,10 @@ var SonicValueElement = class extends SonicFormElement {
506
478
  drag.isOutside = isOutside;
507
479
  this.#renderDragReveal(drag);
508
480
  }
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();
481
+ #endHolds() {
482
+ this.#entry?.close(false);
483
+ this.#pointerDrag?.end();
484
+ this.#endKeyScrub();
519
485
  }
520
486
  #endKeyScrub() {
521
487
  const scrub = this.#keyScrub;
@@ -525,7 +491,7 @@ var SonicValueElement = class extends SonicFormElement {
525
491
  if (this.#value !== scrub.fromValue) this.dispatchEvent(new Event("change", { bubbles: true }));
526
492
  }
527
493
  #isHeld() {
528
- return this.#drag !== void 0 || this.#keyScrub !== void 0;
494
+ return this.#dragging() !== void 0 || this.#keyScrub !== void 0;
529
495
  }
530
496
  #keyTarget(key) {
531
497
  if (resetKeys.has(key)) return this.default;
@@ -539,33 +505,42 @@ var SonicValueElement = class extends SonicFormElement {
539
505
  this.#endKeyScrub();
540
506
  this.#commit(next, next > this.#value ? 1 : -1);
541
507
  }
508
+ #releaseDrag(drag) {
509
+ clearTimeout(drag.revealTimer);
510
+ this.#claim.conceal("drag");
511
+ this.#renderHold();
512
+ if (this.#value !== drag.fromValue) this.dispatchEvent(new Event("change", { bubbles: true }));
513
+ this.#springBack();
514
+ }
542
515
  #renderAria(control, orientation) {
543
516
  this.writeFormValue(String(this.#value), String(this.#value));
544
517
  if (this.#entry?.isOpen) {
545
518
  for (const name of roleAttributes) control.removeAttribute(name);
546
519
  this.forwardNaming(control, false);
547
- this.#renderReadout();
520
+ this.renderReadout();
548
521
  return;
549
522
  }
550
523
  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);
524
+ writeAttribute(control, "role", this.controlRole());
525
+ writeAttribute(control, "aria-valuemin", String(low));
526
+ writeAttribute(control, "aria-valuemax", String(high));
527
+ writeAttribute(control, "aria-valuenow", String(this.#value));
528
+ if (orientation) writeAttribute(control, "aria-orientation", orientation);
556
529
  writeAttribute(control, "aria-valuetext", (this.#formatSpokenValue ?? this.#formatValue)?.(this.#value));
557
530
  this.forwardNaming(control, true);
558
531
  this.#renderDisabled(control);
559
- this.#renderReadout();
532
+ this.renderReadout();
533
+ }
534
+ #renderControl() {
535
+ this.draw();
536
+ this.toggleState("at-origin", !this.isWrapping() && this.#value === this.mapping().snap(this.originValue()));
537
+ this.#renderAria(this.focusTarget(), this.controlOrientation());
560
538
  }
561
539
  #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");
540
+ const isDisabled = this.isDisabled();
541
+ writeAttribute(control, "aria-disabled", isDisabled ? "true" : void 0);
542
+ const stop = this.getAttribute("tabindex") === "-1" ? "-1" : "0";
543
+ writeAttribute(control, "tabindex", isDisabled ? void 0 : stop);
569
544
  }
570
545
  #renderDragReveal(drag) {
571
546
  if (drag.isRevealed && !drag.isOutside) this.#claim.reveal("drag");
@@ -573,33 +548,14 @@ var SonicValueElement = class extends SonicFormElement {
573
548
  this.#renderHold();
574
549
  }
575
550
  #renderHold() {
576
- this.#renderReadout();
551
+ this.renderReadout();
577
552
  this.holdChanged();
578
553
  }
579
- #renderReadout() {
580
- const readout = this.#readout;
581
- if (!readout) return;
582
- const { isOpen, value } = this.#claim.shown(this.#value, this.readout);
583
- readout.bubble.show({
584
- anchor: readout.anchor,
585
- isOpen,
586
- text: value === void 0 ? void 0 : this.#textFor(value)
587
- });
588
- }
589
- #renderScale() {
590
- const marks = this.#scaleMarks?.children ?? [];
591
- const mapping = this.mapping();
592
- for (const mark of marks) {
593
- if (!(mark instanceof HTMLElement || mark instanceof SVGElement)) continue;
594
- const at = String(mapping.proportionOf(scaleValue(mark)));
595
- if (mark.style.getPropertyValue("--_sonic-scale-at") !== at) mark.style.setProperty("--_sonic-scale-at", at);
596
- }
597
- }
598
554
  #reset() {
599
555
  this.#commit(this.default ?? NaN);
600
556
  }
601
557
  #reveal(drag) {
602
- if (drag.isRevealed || this.#drag !== drag) return;
558
+ if (drag.isRevealed || this.#dragging() !== drag) return;
603
559
  drag.isRevealed = true;
604
560
  this.#renderDragReveal(drag);
605
561
  }
@@ -613,6 +569,18 @@ var SonicValueElement = class extends SonicFormElement {
613
569
  }
614
570
  this.input(next);
615
571
  }
572
+ #shown() {
573
+ if (this.isEditing()) return {
574
+ isOpen: true,
575
+ value: void 0
576
+ };
577
+ const isRevealed = this.#claim.isRevealed;
578
+ const preview = isRevealed ? void 0 : this.previewValue();
579
+ return {
580
+ isOpen: (isRevealed || preview !== void 0) && this.readout,
581
+ value: preview ?? this.#value
582
+ };
583
+ }
616
584
  #springBack() {
617
585
  const target = this.springTarget();
618
586
  if (target === void 0) return;
@@ -642,19 +610,6 @@ var SonicValueElement = class extends SonicFormElement {
642
610
  #textFor(value) {
643
611
  return this.#formatValue?.(value) ?? String(value);
644
612
  }
645
- #watchEscape(isHovered) {
646
- if (!isHovered) {
647
- this.#escapeWatch?.abort();
648
- this.#escapeWatch = void 0;
649
- return;
650
- }
651
- if (this.#escapeWatch) return;
652
- const watch = new AbortController();
653
- this.#escapeWatch = watch;
654
- this.ownerDocument.addEventListener("keydown", (event) => {
655
- if (event.key === "Escape" && this.#claim.dismiss()) this.#renderHold();
656
- }, { signal: watch.signal });
657
- }
658
613
  #withinLimit(mapping, state) {
659
614
  const limit = this.#limit;
660
615
  if (!limit || mapping.isWrapping) return state;
@@ -666,8 +621,8 @@ var SonicValueElement = class extends SonicFormElement {
666
621
  }
667
622
  #write(next) {
668
623
  if (!Number.isFinite(next)) return;
669
- this.#isDirty = true;
670
624
  const clamped = this.mapping().snap(next);
625
+ this.#asked = clamped;
671
626
  if (clamped === this.#value) return;
672
627
  this.#value = clamped;
673
628
  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();