@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
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @xsynaptic/sonic-ui
2
2
 
3
- Native custom elements for audio interfaces. Each control renders in the light DOM, drawn in CSS, skinned with `--sonic-*` tokens (CSS custom properties). No framework, no runtime dependencies.
3
+ Native custom elements for audio interfaces. Each control renders in the light DOM, skinned with `--sonic-*` tokens (CSS custom properties). No framework, no runtime dependencies.
4
4
 
5
5
  Early and unstable: any 0.x release may break the API.
6
6
 
@@ -23,4 +23,6 @@ import '@xsynaptic/sonic-ui/define';
23
23
  <sonic-dial aria-label="Cutoff" value="40"></sonic-dial>
24
24
  ```
25
25
 
26
+ Under a hydrating framework, register the definitions once hydration is done (in React, from an effect); a control owns one child of its host and uses only the children it documents.
27
+
26
28
  Targets current Chrome and Edge, Safari 26 and Firefox 147. MIT licensed.
@@ -16,6 +16,7 @@ function readDatHeader(buffer) {
16
16
  if (channels < 1) throw new Error("A .dat header names no channels");
17
17
  const sampleRate = view.getInt32(8, true);
18
18
  const samplesPerPixel = view.getInt32(12, true);
19
+ if (samplesPerPixel < 1) throw new Error("A .dat header names no samples per pixel");
19
20
  return {
20
21
  bits: (view.getUint32(4, true) & 1) === 1 ? 8 : 16,
21
22
  channels,
@@ -34,15 +35,15 @@ function datByteRange(header, fromPair, toPair) {
34
35
  const pairBytes = header.channels * 2 * header.bits / 8;
35
36
  return [header.dataOffset + from * pairBytes, header.dataOffset + to * pairBytes - 1];
36
37
  }
37
- function readDatSamples(header, body) {
38
- return header.bits === 8 ? new Int8Array(body) : new Int16Array(body, 0, Math.floor(body.byteLength / 2));
38
+ function readDatSamples(header, body, byteOffset = 0) {
39
+ return header.bits === 8 ? new Int8Array(body, byteOffset) : new Int16Array(body, byteOffset, Math.floor((body.byteLength - byteOffset) / 2));
39
40
  }
40
- function readDatPeaks(header, body) {
41
+ function readDatPeaks(header, body, byteOffset = 0) {
41
42
  return {
42
43
  channels: header.channels,
43
44
  fullScale: header.bits === 8 ? 128 : 32768,
44
45
  pairsPerSecond: header.pairsPerSecond,
45
- samples: readDatSamples(header, body)
46
+ samples: readDatSamples(header, body, byteOffset)
46
47
  };
47
48
  }
48
49
 
@@ -1,5 +1,5 @@
1
- import { defineOnce } from "../elements/define-once.js";
2
1
  import { SonicButton } from "../elements/button.js";
2
+ import { defineOnce } from "../elements/define-once.js";
3
3
 
4
4
  //#region src/define/button.ts
5
5
  defineOnce("sonic-button", SonicButton);
@@ -1,12 +1,12 @@
1
+ import "./define/button.js";
1
2
  import "./define/dial.js";
2
3
  import "./define/envelope.js";
3
- import "./define/button.js";
4
- import "./define/switch.js";
5
4
  import "./define/meter.js";
6
5
  import "./define/number.js";
7
6
  import "./define/segmented.js";
8
7
  import "./define/slider.js";
9
8
  import "./define/split.js";
9
+ import "./define/switch.js";
10
10
  import "./define/waveform.js";
11
11
  import "./define/wavestrip.js";
12
12
  import "./define/xy.js";
@@ -1,8 +1,9 @@
1
- import { placeChildren, requireChild, template } from "../lib/render.js";
2
1
  import { writeAttribute } from "../lib/write-attribute.js";
3
2
  import { SonicFormElement } from "./form-element.js";
4
3
  import { copyNode } from "../lib/copy-node.js";
5
4
  import { bindHold } from "../lib/hold.js";
5
+ import { placeChildren, requireChild, template } from "../lib/render.js";
6
+ import { mirrorChildren } from "../lib/mirror-children.js";
6
7
 
7
8
  //#region src/elements/button.ts
8
9
  const renderButton = template(`
@@ -88,7 +89,7 @@ var SonicButton = class extends SonicFormElement {
88
89
  const button = this.#button;
89
90
  const cap = requireChild(button, ".sonic-button-cap", HTMLSpanElement);
90
91
  this.upgradeProperties("armed", "busy", "defaultPressed", "momentary", "softDisabled", "latching", "pressed", "value");
91
- this.mirrorChildren({
92
+ mirrorChildren(this, {
92
93
  control: button,
93
94
  copy: copyNode,
94
95
  place: (copies) => {
@@ -113,12 +114,12 @@ var SonicButton = class extends SonicFormElement {
113
114
  return this.#button;
114
115
  }
115
116
  render() {
117
+ if (!this.isBound()) return;
116
118
  const button = this.#button;
117
- button.disabled = this.isDisabled();
119
+ writeAttribute(button, "disabled", this.isDisabled() ? "" : void 0);
118
120
  writeAttribute(button, "aria-disabled", this.#isSoftDisabled() ? "true" : void 0);
119
121
  writeAttribute(button, "aria-busy", this.busy ? "true" : void 0);
120
- button.removeAttribute("aria-pressed");
121
- if (this.latching) button.setAttribute("aria-pressed", String(this.pressed));
122
+ writeAttribute(button, "aria-pressed", this.latching ? String(this.pressed) : void 0);
122
123
  this.toggleState("pressed", this.pressed);
123
124
  this.forwardNaming(button, true);
124
125
  const submitted = this.latching && this.pressed ? this.value : null;
@@ -1,5 +1,6 @@
1
1
  import { requireChild, template } from "../lib/render.js";
2
2
  import { SonicValueElement } from "./value-element.js";
3
+ import { bindScale } from "./scale.js";
3
4
 
4
5
  //#region src/elements/dial.ts
5
6
  const renderDial = template(`
@@ -54,7 +55,10 @@ var SonicDial = class extends SonicValueElement {
54
55
  }
55
56
  connect(signal) {
56
57
  const dial = this.#dial;
57
- this.bindScale(dial, requireChild(dial, ".sonic-dial-scale", HTMLDivElement), signal);
58
+ bindScale(this, {
59
+ control: dial,
60
+ marks: requireChild(dial, ".sonic-dial-scale", HTMLDivElement)
61
+ }, signal);
58
62
  this.render();
59
63
  this.checkStyles(dial, "dial.css");
60
64
  this.bindGestures(dial, signal, () => ({
@@ -1,13 +1,13 @@
1
- import { requireChild, template } from "../lib/render.js";
2
1
  import { writeAttribute } from "../lib/write-attribute.js";
3
2
  import { SonicElement } from "./sonic-element.js";
3
+ import { requireChild, template } from "../lib/render.js";
4
4
  import { ReadoutClaim } from "./readout-claim.js";
5
5
  import { Readout } from "./readout.js";
6
6
  import { dragThresholdPx } from "../lib/drag-step.js";
7
+ import { bindDrag } from "../lib/pointer-drag.js";
7
8
  import { SonicValueElement, linkValue } from "./value-element.js";
8
9
  import { adsrPath, adsrShape, adsrStages, timeStages } from "../lib/adsr-shape.js";
9
10
  import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
10
- import { bindDrag } from "../lib/pointer-drag.js";
11
11
 
12
12
  //#region src/elements/envelope.ts
13
13
  const curveStages = [
@@ -31,10 +31,23 @@ function flip(point, isFlipped) {
31
31
  function isParent(node) {
32
32
  return "querySelector" in node;
33
33
  }
34
+ const emptyPath = {
35
+ fill: "",
36
+ floor: {
37
+ from: 0,
38
+ to: 0
39
+ },
40
+ stroke: ""
41
+ };
42
+ function stageParts(envelope, hook, stages) {
43
+ return new Map(stages.map((stage) => [stage, requireChild(envelope, `.${hook}[data-sonic-stage="${stage}"]`, HTMLDivElement)]));
44
+ }
34
45
  const renderEnvelope = template(`
35
46
  <div class="sonic-envelope" aria-hidden="true">
47
+ <div class="sonic-envelope-bracket"></div>
36
48
  <svg class="sonic-envelope-graph" preserveAspectRatio="none" viewBox="0 0 1 1">
37
49
  <path class="sonic-envelope-fill" />
50
+ <rect class="sonic-envelope-fill sonic-envelope-floor" y="1" />
38
51
  <path class="sonic-envelope-line" />
39
52
  </svg>
40
53
  <div class="sonic-envelope-handle" data-sonic-stage="delay" hidden></div>
@@ -115,12 +128,18 @@ var SonicEnvelope = class extends SonicElement {
115
128
  this.reflect("sustain", id);
116
129
  }
117
130
  #bindings = /* @__PURE__ */ new Map();
131
+ #envelope = renderEnvelope();
132
+ #bracket = requireChild(this.#envelope, ".sonic-envelope-bracket", HTMLDivElement);
118
133
  #claim = new ReadoutClaim(() => {
119
134
  this.#renderReadout();
120
135
  });
121
- #envelope = renderEnvelope();
136
+ #curveHandles = stageParts(this.#envelope, "sonic-envelope-curve", curveStages);
122
137
  #fill = requireChild(this.#envelope, ".sonic-envelope-fill", SVGElement);
138
+ #floor = requireChild(this.#envelope, ".sonic-envelope-floor", SVGElement);
123
139
  #graph = requireChild(this.#envelope, ".sonic-envelope-graph", SVGElement);
140
+ #handles = stageParts(this.#envelope, "sonic-envelope-handle", timeStages);
141
+ #held;
142
+ #isDriving = false;
124
143
  #line = requireChild(this.#envelope, ".sonic-envelope-line", SVGElement);
125
144
  #pointerDrag;
126
145
  #readout = new Readout(requireChild(this.#envelope, ".sonic-envelope-readout", HTMLDivElement));
@@ -145,7 +164,7 @@ var SonicEnvelope = class extends SonicElement {
145
164
  connect(signal) {
146
165
  const envelope = this.#envelope;
147
166
  this.#signal = signal;
148
- this.appendOnce(envelope);
167
+ this.keepControl(envelope, signal);
149
168
  this.#bind();
150
169
  this.checkStyles(envelope, "envelope.css");
151
170
  this.#bindPointer(envelope, signal);
@@ -162,7 +181,7 @@ var SonicEnvelope = class extends SonicElement {
162
181
  if (!element) continue;
163
182
  const link = linkValue(element);
164
183
  const unwatch = link.watch(() => {
165
- this.#draw();
184
+ if (!this.#isDriving) this.#draw();
166
185
  });
167
186
  this.#bindings.set(name, {
168
187
  element,
@@ -202,32 +221,37 @@ var SonicEnvelope = class extends SonicElement {
202
221
  }
203
222
  #draw() {
204
223
  const shape = this.#readShape();
205
- const path = this.#bindings.size === 0 ? {
206
- fill: "",
207
- stroke: ""
208
- } : adsrPath(shape);
224
+ const path = this.#bindings.size === 0 ? emptyPath : adsrPath(shape);
209
225
  this.#shape = shape;
210
- this.#line.setAttribute("d", path.stroke);
211
- this.#fill.setAttribute("d", path.fill);
212
- for (const stage of timeStages) this.#drawPoint(`.sonic-envelope-handle[data-sonic-stage="${stage}"]`, {
226
+ writeAttribute(this.#line, "d", path.stroke);
227
+ writeAttribute(this.#fill, "d", path.fill);
228
+ writeAttribute(this.#floor, "x", String(path.floor.from));
229
+ writeAttribute(this.#floor, "width", String(path.floor.to - path.floor.from));
230
+ for (const [stage, part] of this.#handles) this.#drawPoint(part, {
213
231
  at: shape.handles.find((point) => point.stage === stage),
214
232
  drives: this.#drives(stage)
215
233
  });
216
- for (const stage of curveStages) this.#drawPoint(`.sonic-envelope-curve[data-sonic-stage="${stage}"]`, {
234
+ for (const [stage, part] of this.#curveHandles) this.#drawPoint(part, {
217
235
  at: shape.curveHandles.find((point) => point.stage === stage),
218
236
  drives: this.#curveDrives(stage)
219
237
  });
238
+ this.#drawBracket();
220
239
  this.#renderReadout();
221
240
  }
222
- #drawPoint(selector, point) {
223
- const part = requireChild(this.#envelope, selector, HTMLDivElement);
241
+ #drawBracket() {
242
+ const part = this.#held;
243
+ if (!part) return;
244
+ for (const name of ["--_sonic-envelope-x", "--_sonic-envelope-y"]) this.#bracket.style.setProperty(name, part.style.getPropertyValue(name));
245
+ writeAttribute(this.#bracket, "data-sonic-part", part.classList.contains("sonic-envelope-curve") ? "curve" : void 0);
246
+ }
247
+ #drawPoint(part, point) {
224
248
  const { at, drives } = point;
225
- part.hidden = !at;
249
+ writeAttribute(part, "hidden", at ? void 0 : "");
226
250
  if (!at) return;
227
251
  const isStill = this.isDisabled() || drives.every(({ binding }) => binding.link.isDisabled());
228
252
  part.style.setProperty("--_sonic-envelope-x", String(at.x));
229
253
  part.style.setProperty("--_sonic-envelope-y", String(at.y));
230
- part.dataset.sonicAxes = drives.map(({ axis }) => axis).join("");
254
+ writeAttribute(part, "data-sonic-axes", drives.map(({ axis }) => axis).join(""));
231
255
  writeAttribute(part, "data-sonic-disabled", isStill ? "" : void 0);
232
256
  }
233
257
  #drives(stage) {
@@ -246,6 +270,8 @@ var SonicEnvelope = class extends SonicElement {
246
270
  if (!pressed || pressed.drives.length === 0) return void 0;
247
271
  const { drives, isFlipped, part } = pressed;
248
272
  this.#claim.press(true);
273
+ this.#held = part;
274
+ this.#drawBracket();
249
275
  const box = this.#graph.getBoundingClientRect();
250
276
  const travel = {
251
277
  x: Math.max(1, box.width * this.#shape.share),
@@ -284,11 +310,13 @@ var SonicEnvelope = class extends SonicElement {
284
310
  const step = stepFieldDrag(mappings, drag.state, flip(pointerMove(event), drag.isFlipped));
285
311
  drag.state = step.state;
286
312
  if (step.state.isEngaged) this.#claim.reveal("drag");
313
+ this.#isDriving = true;
287
314
  for (const { axis, binding } of drag.drives) {
288
315
  const next = step[axis];
289
316
  if (next !== void 0) binding.link.input(next);
290
317
  }
291
- this.#renderReadout();
318
+ this.#isDriving = false;
319
+ this.#draw();
292
320
  }
293
321
  #pressed(event) {
294
322
  const part = event.target instanceof Element ? event.target.closest(".sonic-envelope-handle, .sonic-envelope-curve") : void 0;
@@ -1,5 +1,6 @@
1
- import { template } from "../lib/render.js";
1
+ import { toNumber } from "../lib/math.js";
2
2
  import { SonicElement } from "./sonic-element.js";
3
+ import { template } from "../lib/render.js";
3
4
  import { parseNumberList } from "../lib/number-list.js";
4
5
  import { linearTaper } from "../lib/taper.js";
5
6
  import { ballisticsRates, fall, rise, stepNeedle } from "../lib/ballistics.js";
@@ -51,7 +52,8 @@ var SonicMeter = class extends SonicElement {
51
52
  get level() {
52
53
  return this.#level;
53
54
  }
54
- set level(amplitude) {
55
+ set level(level) {
56
+ const amplitude = toNumber(level);
55
57
  if (!Number.isFinite(amplitude)) return;
56
58
  const decibels = toDecibels(amplitude);
57
59
  this.#level = amplitude;
@@ -121,7 +123,8 @@ var SonicMeter = class extends SonicElement {
121
123
  const now = performance.now();
122
124
  if (name === "ballistics") this.#velocity = 0;
123
125
  else if (name === "value") this.#readValue(now);
124
- this.#renderSegments();
126
+ this.#segments = this.segments;
127
+ if (name === "segments") this.#renderSegments();
125
128
  this.#render(now);
126
129
  this.#schedule(now);
127
130
  }
@@ -131,7 +134,7 @@ var SonicMeter = class extends SonicElement {
131
134
  connect(signal) {
132
135
  const now = performance.now();
133
136
  this.upgradeProperties("ballistics", "max", "min", "orientation", "origin", "segments", "value", "level");
134
- this.appendOnce(this.#meter);
137
+ this.keepControl(this.#meter, signal);
135
138
  this.#renderSegments();
136
139
  this.#render(now);
137
140
  this.#schedule(now);
@@ -165,6 +168,7 @@ var SonicMeter = class extends SonicElement {
165
168
  if (value !== void 0) this.#peak = Math.max(this.#peak, value);
166
169
  }
167
170
  #render(now) {
171
+ if (!this.isBound()) return;
168
172
  const meter = this.#meter;
169
173
  const origin = this.origin === void 0 ? 0 : this.#proportionOf(this.origin);
170
174
  meter.style.setProperty("--_sonic-meter-min", String(this.min));
@@ -185,9 +189,9 @@ var SonicMeter = class extends SonicElement {
185
189
  meter.style.setProperty("--_sonic-meter-clipped", now - this.#clipAt < holdMs ? "1" : "0");
186
190
  }
187
191
  #renderSegments() {
192
+ if (!this.isBound()) return;
188
193
  const meter = this.#meter;
189
- const segments = this.segments;
190
- this.#segments = segments;
194
+ const segments = this.#segments;
191
195
  this.toggleState("ladder", segments !== void 0);
192
196
  if (!segments) {
193
197
  for (const name of [
@@ -226,9 +230,9 @@ var SonicMeter = class extends SonicElement {
226
230
  this.#stepNeedle(elapsedMs);
227
231
  return;
228
232
  }
229
- const { fallDecibelsPerSecond, riseMs } = ballisticsRates[mode];
233
+ const { fallDecibelsPerSecond, riseTimeConstantMs } = ballisticsRates[mode];
230
234
  const floor = Math.max(this.#target, this.min);
231
- this.#bar = this.#target > this.#bar ? this.#risen(riseMs, elapsedMs) : fall(this.#bar, {
235
+ this.#bar = this.#target > this.#bar ? this.#risen(riseTimeConstantMs, elapsedMs) : fall(this.#bar, {
232
236
  fallDecibelsPerSecond,
233
237
  floor
234
238
  }, elapsedMs);
@@ -30,7 +30,7 @@ var SonicNumber = class extends SonicValueElement {
30
30
  }
31
31
  connect(signal) {
32
32
  const number = this.#number;
33
- this.appendOnce(number);
33
+ this.keepControl(number, signal);
34
34
  this.render();
35
35
  this.checkStyles(number, "number.css");
36
36
  this.bindGestures(number, signal, () => ({
@@ -1,6 +1,9 @@
1
- import { placeChildren } from "../lib/render.js";
1
+ import { writeAttribute } from "../lib/write-attribute.js";
2
2
  import { SonicFormElement } from "./form-element.js";
3
3
  import { copyNode } from "../lib/copy-node.js";
4
+ import { placeChildren } from "../lib/render.js";
5
+ import { mirrorChildren } from "../lib/mirror-children.js";
6
+ import { bindDrag } from "../lib/pointer-drag.js";
4
7
 
5
8
  //#region src/elements/radio-group.ts
6
9
  const keySteps = /* @__PURE__ */ new Map([
@@ -9,10 +12,12 @@ const keySteps = /* @__PURE__ */ new Map([
9
12
  ["ArrowRight", 1],
10
13
  ["ArrowUp", -1]
11
14
  ]);
12
- function isUnder(option, x, y) {
13
- const box = option.getBoundingClientRect();
15
+ function isInside(box, x, y) {
14
16
  return x >= box.left && x < box.right && y >= box.top && y < box.bottom;
15
17
  }
18
+ function isUnder(option, x, y) {
19
+ return isInside(option.getBoundingClientRect(), x, y);
20
+ }
16
21
  function optionValue(option) {
17
22
  return option.querySelector("[data-sonic-value]")?.dataset.sonicValue;
18
23
  }
@@ -26,7 +31,6 @@ var SonicRadioGroupElement = class extends SonicFormElement {
26
31
  this.#value = next;
27
32
  this.render();
28
33
  }
29
- #press;
30
34
  #value = "";
31
35
  attributeChangedCallback(name) {
32
36
  if (name === "value") this.#value = this.getAttribute("value") ?? "";
@@ -45,7 +49,7 @@ var SonicRadioGroupElement = class extends SonicFormElement {
45
49
  connect(signal) {
46
50
  const group = this.group;
47
51
  this.upgradeProperties("value");
48
- this.mirrorChildren({
52
+ mirrorChildren(this, {
49
53
  control: group,
50
54
  copy: copyNode,
51
55
  isCopied: (child) => child instanceof Element && child.matches("[data-sonic-value]"),
@@ -86,13 +90,15 @@ var SonicRadioGroupElement = class extends SonicFormElement {
86
90
  return option;
87
91
  }
88
92
  render() {
93
+ if (!this.isBound()) return;
89
94
  const options = this.options();
90
95
  const checked = options.find((option) => optionValue(option) === this.#value);
91
96
  const focusable = checked ?? options[0];
97
+ const isDisabled = this.isDisabled();
92
98
  for (const option of options) {
93
- option.disabled = this.isDisabled();
94
- option.setAttribute("aria-checked", String(option === checked));
95
- option.tabIndex = option === focusable ? 0 : -1;
99
+ writeAttribute(option, "disabled", isDisabled ? "" : void 0);
100
+ writeAttribute(option, "aria-checked", String(option === checked));
101
+ writeAttribute(option, "tabindex", option === focusable ? "0" : "-1");
96
102
  }
97
103
  this.forwardNaming(this.group, true);
98
104
  this.writeFormValue(checked ? this.#value : null, this.#value);
@@ -107,41 +113,32 @@ var SonicRadioGroupElement = class extends SonicFormElement {
107
113
  this.dispatchEvent(new Event("change", { bubbles: true }));
108
114
  }
109
115
  #bindPress(group, signal) {
110
- const isOwn = (event) => event.pointerId === this.#press?.pointerId;
111
- const release = (event) => {
112
- if (isOwn(event)) this.#hold(void 0);
113
- };
114
- group.addEventListener("pointerdown", (event) => {
115
- const option = this.optionOf(event.target);
116
- if (!option || event.button !== 0 || this.isDisabled()) return;
117
- if (this.claimPress(option, event)) return;
118
- group.setPointerCapture(event.pointerId);
119
- this.#hold({
120
- option,
121
- pointerId: event.pointerId
122
- });
123
- }, { signal });
124
- group.addEventListener("pointermove", (event) => {
125
- if (!isOwn(event)) return;
126
- this.#hold({
127
- option: this.options().find((option) => isUnder(option, event.clientX, event.clientY)),
128
- pointerId: event.pointerId
129
- });
130
- }, { signal });
131
- group.addEventListener("pointerup", (event) => {
132
- const pressed = isOwn(event) ? this.#press?.option : void 0;
133
- release(event);
134
- if (!pressed || this.isDisabled()) return;
135
- const option = this.releaseTarget(pressed);
136
- if (!option) return;
137
- this.select(option);
138
- if (group.matches(":focus-within")) option.focus();
139
- }, { signal });
140
- group.addEventListener("pointercancel", release, { signal });
141
- group.addEventListener("lostpointercapture", release, { signal });
142
- signal.addEventListener("abort", () => {
143
- this.#hold(void 0);
144
- }, { once: true });
116
+ bindDrag(group, {
117
+ grab: (event) => {
118
+ const option = this.optionOf(event.target);
119
+ if (!option || this.isDisabled() || this.claimPress(option, event)) return;
120
+ const boxes = this.#optionBoxes();
121
+ this.#markPressed(option);
122
+ return {
123
+ boxes,
124
+ option
125
+ };
126
+ },
127
+ lift: (press) => {
128
+ if (!press.option || this.isDisabled()) return;
129
+ const option = this.releaseTarget(press.option);
130
+ if (!option) return;
131
+ this.select(option);
132
+ if (group.matches(":focus-within")) option.focus();
133
+ },
134
+ move: (press, event) => {
135
+ press.option = press.boxes.find(([, box]) => isInside(box, event.clientX, event.clientY))?.[0];
136
+ this.#markPressed(press.option);
137
+ },
138
+ release: () => {
139
+ this.#markPressed(void 0);
140
+ }
141
+ }, signal);
145
142
  }
146
143
  #copyOptions(copies) {
147
144
  const group = this.group;
@@ -160,10 +157,6 @@ var SonicRadioGroupElement = class extends SonicFormElement {
160
157
  this.render();
161
158
  if (focused) this.#refocus(focused, focusedValue);
162
159
  }
163
- #hold(press) {
164
- this.#press = press;
165
- for (const option of this.options()) option.toggleAttribute("data-sonic-pressed", option === press?.option);
166
- }
167
160
  #keyTarget(option, key) {
168
161
  const options = this.options();
169
162
  if (key === "Home") return options[0];
@@ -174,6 +167,12 @@ var SonicRadioGroupElement = class extends SonicFormElement {
174
167
  if (this.isWrapping()) return options[(index + options.length) % options.length];
175
168
  return options[Math.min(Math.max(index, 0), options.length - 1)];
176
169
  }
170
+ #markPressed(pressed) {
171
+ for (const option of this.options()) option.toggleAttribute("data-sonic-pressed", option === pressed);
172
+ }
173
+ #optionBoxes() {
174
+ return this.options().map((option) => [option, option.getBoundingClientRect()]);
175
+ }
177
176
  #refocus(focused, value) {
178
177
  const options = this.options();
179
178
  const next = options.find((option) => optionValue(option) === value) ?? options.find((option) => option.tabIndex === 0);
@@ -6,10 +6,7 @@ var ReadoutClaim = class {
6
6
  return this.#isDragging || this.#keyTimer !== void 0;
7
7
  }
8
8
  #holdTimer;
9
- #hover;
10
- #isDismissed = false;
11
9
  #isDragging = false;
12
- #isEditing = false;
13
10
  #keyTimer;
14
11
  #onLapse;
15
12
  constructor(onLapse) {
@@ -27,21 +24,6 @@ var ReadoutClaim = class {
27
24
  this.#keyTimer = void 0;
28
25
  return wasTimed;
29
26
  }
30
- dismiss() {
31
- const wasHovered = this.#hover !== void 0;
32
- this.#hover = void 0;
33
- this.#isDismissed = wasHovered || this.#isDismissed;
34
- return wasHovered;
35
- }
36
- edit(isOpen) {
37
- this.#isEditing = isOpen;
38
- if (isOpen) this.#hover = void 0;
39
- }
40
- hover(value) {
41
- if (value === void 0) this.#isDismissed = false;
42
- if (this.#isDismissed || this.#isEditing || this.isRevealed) return;
43
- this.#hover = value;
44
- }
45
27
  press(isDown) {
46
28
  this.conceal("drag");
47
29
  if (!isDown) {
@@ -54,7 +36,6 @@ var ReadoutClaim = class {
54
36
  }, 250);
55
37
  }
56
38
  reveal(by) {
57
- this.#hover = void 0;
58
39
  if (by === "drag") {
59
40
  this.#isDragging = true;
60
41
  return;
@@ -65,16 +46,6 @@ var ReadoutClaim = class {
65
46
  this.#onLapse();
66
47
  }, keyRevealMs);
67
48
  }
68
- shown(current, isReadout) {
69
- if (this.#isEditing) return {
70
- isOpen: true,
71
- value: void 0
72
- };
73
- return {
74
- isOpen: (this.isRevealed || this.#hover !== void 0) && isReadout,
75
- value: this.#hover ?? current
76
- };
77
- }
78
49
  };
79
50
 
80
51
  //#endregion
@@ -23,7 +23,7 @@ var Readout = class {
23
23
  this.#anchor = anchor;
24
24
  }
25
25
  this.#text.hidden = text === void 0;
26
- if (text !== void 0) this.#text.textContent = text;
26
+ if (isOpen && text !== void 0 && this.#text.textContent !== text) this.#text.textContent = text;
27
27
  this.#toggle(isOpen);
28
28
  }
29
29
  #toggle(isOpen) {
@@ -0,0 +1,43 @@
1
+ import { copyNode } from "../lib/copy-node.js";
2
+ import { placeChildren } from "../lib/render.js";
3
+ import { mirrorChildren } from "../lib/mirror-children.js";
4
+ import { linkValue } from "./value-element.js";
5
+
6
+ //#region src/elements/scale.ts
7
+ function scaleValue(mark) {
8
+ const text = mark.dataset.sonicValue?.trim();
9
+ return text ? Number(text) : NaN;
10
+ }
11
+ function scaleMark(original) {
12
+ const mark = copyNode(original);
13
+ if (!(mark instanceof HTMLElement || mark instanceof SVGElement)) return void 0;
14
+ if (!Number.isFinite(scaleValue(mark))) return void 0;
15
+ const isTick = mark.childElementCount === 0 && mark.textContent.trim() === "";
16
+ mark.classList.add(isTick ? "sonic-scale-tick" : "sonic-scale-label");
17
+ return mark;
18
+ }
19
+ function bindScale(element, { control, marks }, signal) {
20
+ const { mapping, watch } = linkValue(element);
21
+ const render = () => {
22
+ const { proportionOf } = mapping();
23
+ for (const mark of marks.children) {
24
+ if (!(mark instanceof HTMLElement || mark instanceof SVGElement)) continue;
25
+ const at = String(proportionOf(scaleValue(mark)));
26
+ if (mark.style.getPropertyValue("--_sonic-scale-at") !== at) mark.style.setProperty("--_sonic-scale-at", at);
27
+ }
28
+ };
29
+ mirrorChildren(element, {
30
+ control,
31
+ copy: scaleMark,
32
+ isCopied: (child) => child instanceof Element && child.matches("[data-sonic-value]"),
33
+ isPassed: (child) => child instanceof Element && child.matches(".sonic-led"),
34
+ place: (copies) => {
35
+ placeChildren(marks, copies);
36
+ render();
37
+ }
38
+ }, signal);
39
+ signal.addEventListener("abort", watch(render), { once: true });
40
+ }
41
+
42
+ //#endregion
43
+ export { bindScale };
@@ -1,5 +1,7 @@
1
1
  import { requireChild, template } from "../lib/render.js";
2
2
  import { SonicValueElement } from "./value-element.js";
3
+ import { bindScale } from "./scale.js";
4
+ import { sortedRegions } from "../lib/time-regions.js";
3
5
 
4
6
  //#region src/elements/slider.ts
5
7
  function grooveStop(proportion) {
@@ -34,6 +36,13 @@ var SonicSlider = class extends SonicValueElement {
34
36
  "orientation",
35
37
  "spring"
36
38
  ];
39
+ get buffered() {
40
+ return this.#buffered.map(([start, end]) => [start, end]);
41
+ }
42
+ set buffered(regions) {
43
+ this.#buffered = sortedRegions(regions);
44
+ this.render();
45
+ }
37
46
  get groovePress() {
38
47
  return this.getAttribute("groove-press") === "none" ? "none" : "jump";
39
48
  }
@@ -64,15 +73,19 @@ var SonicSlider = class extends SonicValueElement {
64
73
  set spring(isSpring) {
65
74
  this.reflect("spring", isSpring);
66
75
  }
76
+ #buffered = [];
67
77
  #slider = renderSlider();
68
78
  connectedCallback() {
69
- this.upgradeProperties("groovePress", "modulationValue", "modulation", "orientation", "spring");
79
+ this.upgradeProperties("buffered", "groovePress", "modulationValue", "modulation", "orientation", "spring");
70
80
  super.connectedCallback();
71
81
  }
72
82
  connect(signal) {
73
83
  const slider = this.#slider;
74
84
  const cap = requireChild(slider, ".sonic-slider-cap", HTMLDivElement);
75
- this.bindScale(slider, requireChild(slider, ".sonic-slider-scale", HTMLDivElement), signal);
85
+ bindScale(this, {
86
+ control: slider,
87
+ marks: requireChild(slider, ".sonic-slider-scale", HTMLDivElement)
88
+ }, signal);
76
89
  slider.addEventListener("transitionend", (event) => {
77
90
  if (event.propertyName === "--_sonic-slider-value") this.toggleState("springing", false);
78
91
  }, { signal });
@@ -113,7 +126,7 @@ var SonicSlider = class extends SonicValueElement {
113
126
  };
114
127
  }
115
128
  #renderBuffered(slider) {
116
- const buffered = this.buffered;
129
+ const buffered = this.#buffered;
117
130
  if (buffered.length === 0) {
118
131
  slider.style.removeProperty("--_sonic-slider-buffered-regions");
119
132
  return;