@xsynaptic/sonic-ui 0.6.0 → 0.8.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 +4 -0
  2. package/dist/default/define/spectrum.js +7 -0
  3. package/dist/default/define.js +1 -0
  4. package/dist/default/elements/button.js +33 -4
  5. package/dist/default/elements/dial.js +5 -1
  6. package/dist/default/elements/display-element.js +77 -0
  7. package/dist/default/elements/envelope.js +15 -3
  8. package/dist/default/elements/meter.js +1 -1
  9. package/dist/default/elements/radio-group.js +3 -2
  10. package/dist/default/elements/readout-claim.js +0 -29
  11. package/dist/default/elements/scale.js +43 -0
  12. package/dist/default/elements/slider.js +82 -3
  13. package/dist/default/elements/sonic-element.js +2 -67
  14. package/dist/default/elements/spectrum.js +230 -0
  15. package/dist/default/elements/switch.js +1 -1
  16. package/dist/default/elements/value-element.js +81 -91
  17. package/dist/default/elements/wave-element.js +59 -9
  18. package/dist/default/elements/waveform.js +56 -10
  19. package/dist/default/elements/wavestrip.js +45 -21
  20. package/dist/default/elements/xy.js +3 -2
  21. package/dist/default/index.js +2 -1
  22. package/dist/default/lib/canvas-surface.js +7 -4
  23. package/dist/default/lib/frame-timeline.js +2 -2
  24. package/dist/default/lib/hover-preview.js +46 -0
  25. package/dist/default/lib/marker-rider.js +7 -4
  26. package/dist/default/lib/mirror-children.js +64 -0
  27. package/dist/default/lib/owned-control.js +10 -0
  28. package/dist/default/lib/pointer-drag.js +7 -2
  29. package/dist/default/lib/spectrum-columns.js +74 -0
  30. package/dist/default/lib/spectrum-levels.js +55 -0
  31. package/dist/default/lib/strip-scene.js +12 -1
  32. package/dist/default/lib/time-regions.js +8 -1
  33. package/dist/dev/define/spectrum.d.ts +2 -0
  34. package/dist/dev/define/spectrum.js +8 -0
  35. package/dist/dev/define/spectrum.js.map +1 -0
  36. package/dist/dev/define.d.ts +3 -1
  37. package/dist/dev/define.js +1 -0
  38. package/dist/dev/elements/button.d.ts +2 -0
  39. package/dist/dev/elements/button.js +33 -4
  40. package/dist/dev/elements/button.js.map +1 -1
  41. package/dist/dev/elements/dial.js +5 -1
  42. package/dist/dev/elements/dial.js.map +1 -1
  43. package/dist/dev/elements/display-element.d.ts +29 -0
  44. package/dist/dev/elements/display-element.js +78 -0
  45. package/dist/dev/elements/display-element.js.map +1 -0
  46. package/dist/dev/elements/envelope.js +15 -3
  47. package/dist/dev/elements/envelope.js.map +1 -1
  48. package/dist/dev/elements/meter.js +1 -1
  49. package/dist/dev/elements/radio-group.js +3 -2
  50. package/dist/dev/elements/radio-group.js.map +1 -1
  51. package/dist/dev/elements/readout-claim.js +0 -29
  52. package/dist/dev/elements/readout-claim.js.map +1 -1
  53. package/dist/dev/elements/scale.js +44 -0
  54. package/dist/dev/elements/scale.js.map +1 -0
  55. package/dist/dev/elements/slider.d.ts +9 -0
  56. package/dist/dev/elements/slider.js +82 -3
  57. package/dist/dev/elements/slider.js.map +1 -1
  58. package/dist/dev/elements/sonic-element.d.ts +0 -8
  59. package/dist/dev/elements/sonic-element.js +3 -90
  60. package/dist/dev/elements/sonic-element.js.map +1 -1
  61. package/dist/dev/elements/spectrum.d.ts +59 -0
  62. package/dist/dev/elements/spectrum.js +231 -0
  63. package/dist/dev/elements/spectrum.js.map +1 -0
  64. package/dist/dev/elements/switch.js +1 -1
  65. package/dist/dev/elements/value-element.d.ts +8 -6
  66. package/dist/dev/elements/value-element.js +81 -91
  67. package/dist/dev/elements/value-element.js.map +1 -1
  68. package/dist/dev/elements/wave-element.d.ts +8 -2
  69. package/dist/dev/elements/wave-element.js +59 -9
  70. package/dist/dev/elements/wave-element.js.map +1 -1
  71. package/dist/dev/elements/waveform.d.ts +12 -4
  72. package/dist/dev/elements/waveform.js +56 -10
  73. package/dist/dev/elements/waveform.js.map +1 -1
  74. package/dist/dev/elements/wavestrip.d.ts +7 -0
  75. package/dist/dev/elements/wavestrip.js +45 -21
  76. package/dist/dev/elements/wavestrip.js.map +1 -1
  77. package/dist/dev/elements/xy.js +3 -2
  78. package/dist/dev/elements/xy.js.map +1 -1
  79. package/dist/dev/index.d.ts +4 -1
  80. package/dist/dev/index.js +2 -1
  81. package/dist/dev/lib/canvas-surface.d.ts +4 -3
  82. package/dist/dev/lib/canvas-surface.js +7 -4
  83. package/dist/dev/lib/canvas-surface.js.map +1 -1
  84. package/dist/dev/lib/frame-timeline.js +2 -2
  85. package/dist/dev/lib/frame-timeline.js.map +1 -1
  86. package/dist/dev/lib/hover-preview.js +47 -0
  87. package/dist/dev/lib/hover-preview.js.map +1 -0
  88. package/dist/dev/lib/marker-rider.js +7 -4
  89. package/dist/dev/lib/marker-rider.js.map +1 -1
  90. package/dist/dev/lib/mirror-children.js +80 -0
  91. package/dist/dev/lib/mirror-children.js.map +1 -0
  92. package/dist/dev/lib/owned-control.js +23 -0
  93. package/dist/dev/lib/owned-control.js.map +1 -0
  94. package/dist/dev/lib/pointer-drag.js +7 -2
  95. package/dist/dev/lib/pointer-drag.js.map +1 -1
  96. package/dist/dev/lib/spectrum-columns.js +75 -0
  97. package/dist/dev/lib/spectrum-columns.js.map +1 -0
  98. package/dist/dev/lib/spectrum-levels.js +56 -0
  99. package/dist/dev/lib/spectrum-levels.js.map +1 -0
  100. package/dist/dev/lib/strip-scene.js +12 -1
  101. package/dist/dev/lib/strip-scene.js.map +1 -1
  102. package/dist/dev/lib/time-regions.js +8 -1
  103. package/dist/dev/lib/time-regions.js.map +1 -1
  104. package/dist/skins/flat.css +2 -1
  105. package/dist/styles/button.css +28 -1
  106. package/dist/styles/controls.css +1 -0
  107. package/dist/styles/envelope.css +30 -30
  108. package/dist/styles/led.css +18 -2
  109. package/dist/styles/material.css +90 -29
  110. package/dist/styles/meter.css +22 -18
  111. package/dist/styles/panel.css +2 -1
  112. package/dist/styles/slider.css +42 -5
  113. package/dist/styles/spectrum.css +105 -0
  114. package/dist/styles/switch.css +10 -7
  115. package/dist/styles/waveform.css +38 -16
  116. package/dist/styles/wavestrip.css +22 -4
  117. package/dist/styles/xy.css +14 -30
  118. package/package.json +2 -1
@@ -0,0 +1,230 @@
1
+ import { requireChild, template } from "../lib/render.js";
2
+ import { SonicDisplayElement } from "./display-element.js";
3
+ import { columnEdges, columnLevels, gridColumns, gridRows, levelRows, loudestBin, zoneRows } from "../lib/spectrum-columns.js";
4
+ import { areSettled, createLevels, receiveLevels, stepLevels } from "../lib/spectrum-levels.js";
5
+
6
+ //#region src/elements/spectrum.ts
7
+ const colours = {
8
+ clip: "--_sonic-spectrum-clip",
9
+ grid: "--_sonic-spectrum-grid",
10
+ hot: "--_sonic-spectrum-hot",
11
+ lit: "--_sonic-spectrum-lit"
12
+ };
13
+ const numbers = {
14
+ clipFrom: "--_sonic-spectrum-clip-from",
15
+ hotFrom: "--_sonic-spectrum-hot-from"
16
+ };
17
+ const defaultFrequencies = [20, 2e4];
18
+ const defaultSampleRate = 48e3;
19
+ const renderSpectrum = template(`
20
+ <div aria-hidden="true" class="sonic-spectrum">
21
+ <canvas class="sonic-spectrum-canvas"></canvas>
22
+ </div>
23
+ `, HTMLDivElement);
24
+ function paintGrid(context, { dpr, height, look, range, rowOf, width }, [frequencyMin, frequencyMax]) {
25
+ const line = Math.max(1, Math.round(dpr));
26
+ const columns = gridColumns({
27
+ frequencyMax,
28
+ frequencyMin
29
+ }, width);
30
+ const rows = gridRows(range, rowOf);
31
+ context.beginPath();
32
+ for (const column of columns) context.rect(column, 0, line, height);
33
+ for (const row of rows) context.rect(0, row, width, line);
34
+ context.fillStyle = look.colours.grid;
35
+ context.fill();
36
+ }
37
+ function zoneFill(context, { height, look, rowOf }) {
38
+ const gradient = context.createLinearGradient(0, 0, 0, height);
39
+ const { clip, hot } = zoneRows(look.numbers, rowOf);
40
+ gradient.addColorStop(clip / height, look.colours.clip);
41
+ gradient.addColorStop(clip / height, look.colours.hot);
42
+ gradient.addColorStop(hot / height, look.colours.hot);
43
+ gradient.addColorStop(hot / height, look.colours.lit);
44
+ return gradient;
45
+ }
46
+ function paintBars(context, scene, bars) {
47
+ const { height, rowOf } = scene;
48
+ let previous = height;
49
+ context.beginPath();
50
+ context.moveTo(0, height);
51
+ for (const [column, level] of bars.entries()) {
52
+ const row = rowOf(level);
53
+ bars[column] = row;
54
+ if (row === previous) continue;
55
+ context.lineTo(column, previous);
56
+ context.lineTo(column, row);
57
+ previous = row;
58
+ }
59
+ context.lineTo(bars.length, previous);
60
+ context.lineTo(bars.length, height);
61
+ context.closePath();
62
+ context.fillStyle = zoneFill(context, scene);
63
+ context.fill();
64
+ }
65
+ function paintHolds(context, { look, rowOf }, { bars, holds }) {
66
+ context.beginPath();
67
+ for (const [column, level] of holds.entries()) {
68
+ const row = rowOf(level);
69
+ if (row < (bars[column] ?? 0)) context.rect(column, row, 1, 1);
70
+ }
71
+ context.fillStyle = look.colours.lit;
72
+ context.fill();
73
+ }
74
+ var SonicSpectrum = class extends SonicDisplayElement {
75
+ static observedAttributes = [
76
+ ...SonicDisplayElement.observedAttributes,
77
+ "frequency-max",
78
+ "frequency-min",
79
+ "grid",
80
+ "peak-hold",
81
+ "sample-rate"
82
+ ];
83
+ get frequencyMax() {
84
+ return this.numberAttribute("frequency-max", defaultFrequencies[1]);
85
+ }
86
+ set frequencyMax(hertz) {
87
+ this.reflect("frequency-max", hertz);
88
+ }
89
+ get frequencyMin() {
90
+ return this.numberAttribute("frequency-min", defaultFrequencies[0]);
91
+ }
92
+ set frequencyMin(hertz) {
93
+ this.reflect("frequency-min", hertz);
94
+ }
95
+ get grid() {
96
+ return this.getAttribute("grid") === "none" ? "none" : "on";
97
+ }
98
+ set grid(grid) {
99
+ this.reflect("grid", grid);
100
+ }
101
+ get peak() {
102
+ return this.#peak && { ...this.#peak };
103
+ }
104
+ get peakHold() {
105
+ return this.hasAttribute("peak-hold");
106
+ }
107
+ set peakHold(isHeld) {
108
+ this.reflect("peak-hold", isHeld);
109
+ }
110
+ get sampleRate() {
111
+ const sampleRate = this.numberAttribute("sample-rate", defaultSampleRate);
112
+ return sampleRate > 0 ? sampleRate : defaultSampleRate;
113
+ }
114
+ set sampleRate(hertz) {
115
+ this.reflect("sample-rate", hertz);
116
+ }
117
+ control = renderSpectrum();
118
+ canvas = requireChild(this.control, ".sonic-spectrum-canvas", HTMLCanvasElement);
119
+ colours = colours;
120
+ numbers = numbers;
121
+ sheet = "spectrum.css";
122
+ #columns;
123
+ #isSettled = true;
124
+ #lastFrameMs;
125
+ #levels = createLevels(0);
126
+ #peak;
127
+ #receivedRate;
128
+ connectedCallback() {
129
+ this.upgradeProperties("frequencyMax", "frequencyMin", "grid", "peakHold", "sampleRate");
130
+ super.connectedCallback();
131
+ }
132
+ resetPeak() {
133
+ this.#peak = void 0;
134
+ }
135
+ paint(context, { frameMs, isDirty, look, size }) {
136
+ if (this.isDisabled()) {
137
+ if (isDirty) this.#draw(context, look, size);
138
+ return;
139
+ }
140
+ const wasSettled = this.#isSettled;
141
+ this.#step(frameMs, look.isReducedMotion);
142
+ if (isDirty || !wasSettled || !this.#isSettled) this.#draw(context, look, size);
143
+ if (!this.#isSettled) this.surface()?.requestFrame();
144
+ }
145
+ pull(analyser, buffer) {
146
+ analyser.getFloatFrequencyData(buffer);
147
+ }
148
+ receive(frame) {
149
+ if (frame.length !== this.#levels.bars.length) this.#levels = createLevels(frame.length);
150
+ this.#receivedRate = this.analyser?.context.sampleRate ?? this.sampleRate;
151
+ receiveLevels(this.#levels, frame, this.min);
152
+ const loudest = loudestBin(frame, this.#axis());
153
+ if (loudest && loudest.decibels > (this.#peak?.decibels ?? -Infinity)) this.#peak = loudest;
154
+ }
155
+ #axis() {
156
+ const [frequencyMin, frequencyMax] = this.#frequencies();
157
+ return {
158
+ binCount: this.#levels.bars.length,
159
+ frequencyMax,
160
+ frequencyMin,
161
+ sampleRate: this.#receivedRate ?? this.sampleRate
162
+ };
163
+ }
164
+ #columnsFor(width) {
165
+ const axis = this.#axis();
166
+ const key = [
167
+ width,
168
+ axis.binCount,
169
+ axis.sampleRate,
170
+ axis.frequencyMin,
171
+ axis.frequencyMax
172
+ ].join(" ");
173
+ if (this.#columns?.key !== key) this.#columns = {
174
+ bars: new Float64Array(width),
175
+ edges: columnEdges(axis, width),
176
+ holds: new Float64Array(width),
177
+ key
178
+ };
179
+ return this.#columns;
180
+ }
181
+ #draw(context, look, size) {
182
+ const scene = {
183
+ ...size,
184
+ look,
185
+ range: {
186
+ max: this.max,
187
+ min: this.min
188
+ },
189
+ rowOf: levelRows({
190
+ max: this.max,
191
+ min: this.min
192
+ }, size.height)
193
+ };
194
+ context.clearRect(0, 0, size.width, size.height);
195
+ if (this.grid === "on") paintGrid(context, scene, this.#frequencies());
196
+ if (this.isDisabled()) return;
197
+ const { bars, edges, holds } = this.#columnsFor(size.width);
198
+ const spread = {
199
+ edges,
200
+ floor: scene.range.min
201
+ };
202
+ columnLevels(bars, this.#levels.bars, spread);
203
+ paintBars(context, scene, bars);
204
+ if (!this.peakHold) return;
205
+ columnLevels(holds, this.#levels.holds, spread);
206
+ paintHolds(context, scene, {
207
+ bars,
208
+ holds
209
+ });
210
+ }
211
+ #frequencies() {
212
+ const { frequencyMax, frequencyMin } = this;
213
+ return frequencyMin > 0 && frequencyMax > frequencyMin ? [frequencyMin, frequencyMax] : defaultFrequencies;
214
+ }
215
+ #step(frameMs, isReducedMotion) {
216
+ const floor = this.min;
217
+ stepLevels(this.#levels, {
218
+ elapsedMs: Math.max(0, frameMs - (this.#lastFrameMs ?? frameMs)),
219
+ floor,
220
+ isHeld: this.peakHold,
221
+ isHoldStepped: isReducedMotion,
222
+ nowMs: frameMs
223
+ });
224
+ this.#isSettled = areSettled(this.#levels, floor);
225
+ this.#lastFrameMs = this.#isSettled ? void 0 : frameMs;
226
+ }
227
+ };
228
+
229
+ //#endregion
230
+ export { SonicSpectrum };
@@ -1,7 +1,7 @@
1
1
  import { clamp } from "../lib/math.js";
2
- import { requireChild, template } from "../lib/render.js";
3
2
  import { writeAttribute } from "../lib/write-attribute.js";
4
3
  import { bindHold } from "../lib/hold.js";
4
+ import { requireChild, template } from "../lib/render.js";
5
5
  import { bindDrag } from "../lib/pointer-drag.js";
6
6
  import { SonicRadioGroupElement, isUnder, optionValue } from "./radio-group.js";
7
7
 
@@ -1,8 +1,8 @@
1
- import { clamp, toNumber } from "../lib/math.js";
2
- import { placeChildren, requireChild } from "../lib/render.js";
1
+ import { clamp, toNumber, trimFloat } from "../lib/math.js";
3
2
  import { writeAttribute } from "../lib/write-attribute.js";
4
3
  import { SonicFormElement } from "./form-element.js";
5
- import { copyNode } from "../lib/copy-node.js";
4
+ import { requireChild } from "../lib/render.js";
5
+ import { bindDrag } from "../lib/pointer-drag.js";
6
6
  import { ReadoutClaim } from "./readout-claim.js";
7
7
  import { Readout } from "./readout.js";
8
8
  import { ValueEntry } from "./value-entry.js";
@@ -10,9 +10,7 @@ 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";
14
13
  import { readPxProperty } from "../lib/read-px-property.js";
15
- import { readRegions } from "../lib/time-regions.js";
16
14
  import { resetKeys, valueMapping } from "../lib/value-mapping.js";
17
15
 
18
16
  //#region src/elements/value-element.ts
@@ -37,18 +35,6 @@ const roleAttributes = [
37
35
  "role",
38
36
  "tabindex"
39
37
  ];
40
- function scaleValue(mark) {
41
- const text = mark.dataset.sonicValue?.trim();
42
- return text ? Number(text) : NaN;
43
- }
44
- function scaleMark(original) {
45
- const mark = copyNode(original);
46
- if (!(mark instanceof HTMLElement || mark instanceof SVGElement)) return void 0;
47
- if (!Number.isFinite(scaleValue(mark))) return void 0;
48
- const isTick = mark.childElementCount === 0 && mark.textContent.trim() === "";
49
- mark.classList.add(isTick ? "sonic-scale-tick" : "sonic-scale-label");
50
- return mark;
51
- }
52
38
  let link;
53
39
  function linkValue(element) {
54
40
  return link(element);
@@ -63,7 +49,9 @@ var SonicValueElement = class extends SonicFormElement {
63
49
  "notched",
64
50
  "origin",
65
51
  "readout",
52
+ "spoken-step",
66
53
  "step",
54
+ "tabindex",
67
55
  "taper",
68
56
  "value",
69
57
  "positions"
@@ -89,13 +77,6 @@ var SonicValueElement = class extends SonicFormElement {
89
77
  }
90
78
  });
91
79
  }
92
- get buffered() {
93
- return this.#buffered.map(([start, end]) => [start, end]);
94
- }
95
- set buffered(regions) {
96
- this.#buffered = regions ? readRegions(regions).toSorted((first, second) => first[0] - second[0]) : [];
97
- this.render();
98
- }
99
80
  get default() {
100
81
  return this.optionalNumberAttribute("default");
101
82
  }
@@ -176,6 +157,9 @@ var SonicValueElement = class extends SonicFormElement {
176
157
  set parseValue(parse) {
177
158
  this.#parseValue = parse;
178
159
  }
160
+ get pointerType() {
161
+ return this.#dragging()?.pointerType;
162
+ }
179
163
  get positions() {
180
164
  return parseNumberList(this.getAttribute("positions"));
181
165
  }
@@ -188,6 +172,12 @@ var SonicValueElement = class extends SonicFormElement {
188
172
  set readout(isShown) {
189
173
  this.reflect("readout", isShown);
190
174
  }
175
+ get spokenStep() {
176
+ return this.optionalNumberAttribute("spoken-step");
177
+ }
178
+ set spokenStep(value) {
179
+ this.reflect("spoken-step", value);
180
+ }
191
181
  get step() {
192
182
  return this.numberAttribute("step", 1);
193
183
  }
@@ -220,16 +210,16 @@ var SonicValueElement = class extends SonicFormElement {
220
210
  return this.#textFor(this.#value);
221
211
  }
222
212
  #asked;
223
- #buffered = [];
224
213
  #claim = new ReadoutClaim(() => {
225
214
  this.#renderHold();
226
215
  });
227
216
  #entry;
228
- #escapeWatch;
229
217
  #formatSpokenValue;
230
218
  #formatValue;
219
+ #heldAt;
231
220
  #instance = String(instanceCount += 1);
232
221
  #isModulated = false;
222
+ #isRevealed = false;
233
223
  #keyScrub;
234
224
  #land;
235
225
  #limit;
@@ -239,7 +229,6 @@ var SonicValueElement = class extends SonicFormElement {
239
229
  #pointerDrag;
240
230
  #positions;
241
231
  #readout;
242
- #scaleMarks;
243
232
  #value = 0;
244
233
  #watchers = /* @__PURE__ */ new Set();
245
234
  attributeChangedCallback(name) {
@@ -258,7 +247,7 @@ var SonicValueElement = class extends SonicFormElement {
258
247
  this.render();
259
248
  }
260
249
  connectedCallback() {
261
- this.upgradeProperties("buffered", "default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "step", "taper", "positions", "detent", "formatValue", "formatSpokenValue", "keyStep", "parseValue", "value");
250
+ this.upgradeProperties("default", "dimmed", "doublePress", "max", "midpoint", "min", "notched", "origin", "readout", "spokenStep", "step", "taper", "positions", "detent", "formatValue", "formatSpokenValue", "keyStep", "parseValue", "value");
262
251
  super.connectedCallback();
263
252
  }
264
253
  bindGestures(control, signal, grab) {
@@ -284,6 +273,7 @@ var SonicValueElement = class extends SonicFormElement {
284
273
  fromValue,
285
274
  isOutside: false,
286
275
  isRevealed: false,
276
+ pointerType: event.pointerType,
287
277
  revealTimer: void 0,
288
278
  state: this.#startDrag(control, axis, event)
289
279
  };
@@ -322,19 +312,6 @@ var SonicValueElement = class extends SonicFormElement {
322
312
  this.#concealKeyReveal();
323
313
  }, { once: true });
324
314
  }
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
- isPassed: (child) => child instanceof Element && child.matches(".sonic-led"),
332
- place: (copies) => {
333
- placeChildren(marks, copies);
334
- this.#renderScale();
335
- }
336
- }, signal);
337
- }
338
315
  controlOrientation() {}
339
316
  controlRole() {
340
317
  return "slider";
@@ -342,13 +319,10 @@ var SonicValueElement = class extends SonicFormElement {
342
319
  heldFrom() {
343
320
  return this.#dragging()?.fromValue ?? this.#keyScrub?.fromValue;
344
321
  }
345
- heldWrite(_next) {}
346
- holdChanged() {}
347
- hoverReadout(value) {
348
- this.#claim.hover(value);
349
- this.#renderReadout();
350
- this.#watchEscape(value !== void 0);
322
+ heldWrite(next) {
323
+ this.#heldAt = next;
351
324
  }
325
+ holdChanged() {}
352
326
  input(next) {
353
327
  const previous = this.#value;
354
328
  this.#write(this.#limit ? clamp(next, ...this.#limit) : next);
@@ -359,6 +333,9 @@ var SonicValueElement = class extends SonicFormElement {
359
333
  }));
360
334
  return true;
361
335
  }
336
+ isEditing() {
337
+ return this.#entry?.isOpen === true;
338
+ }
362
339
  isRevealed() {
363
340
  return this.#claim.isRevealed;
364
341
  }
@@ -384,17 +361,33 @@ var SonicValueElement = class extends SonicFormElement {
384
361
  originValue() {
385
362
  return this.origin ?? this.mapping().bounds[0];
386
363
  }
364
+ playback() {
365
+ const from = this.heldFrom();
366
+ return from === void 0 ? this.#value : this.#heldAt ?? from;
367
+ }
368
+ previewValue() {}
387
369
  proportionsChanged() {}
388
370
  readModulationValue() {
389
371
  return this.#modulationValue;
390
372
  }
391
373
  readoutValue() {
392
- return this.#claim.shown(this.#value, this.readout).value ?? this.#value;
374
+ return this.#shown().value ?? this.#value;
393
375
  }
394
376
  render() {
395
377
  if (this.isBound()) this.#renderControl();
396
378
  for (const listener of this.#watchers) listener();
397
379
  }
380
+ renderReadout() {
381
+ const readout = this.#readout;
382
+ if (!readout) return;
383
+ const { isOpen, value } = this.#shown();
384
+ const text = value === void 0 ? void 0 : this.#textFor(value);
385
+ readout.bubble.show({
386
+ anchor: readout.anchor,
387
+ isOpen: isOpen && text !== "",
388
+ text
389
+ });
390
+ }
398
391
  restoreState(state) {
399
392
  this.value = Number(state);
400
393
  }
@@ -440,7 +433,6 @@ var SonicValueElement = class extends SonicFormElement {
440
433
  },
441
434
  text: () => this.#parseValue ? this.valueText : String(this.#value),
442
435
  toggle: (isOpen) => {
443
- this.#claim.edit(isOpen);
444
436
  if (isOpen) this.forwardNaming(input, true);
445
437
  this.toggleState("editing", isOpen);
446
438
  this.render();
@@ -501,10 +493,13 @@ var SonicValueElement = class extends SonicFormElement {
501
493
  drag.state = this.#withinLimit(mapping, state);
502
494
  if (value === void 0) return;
503
495
  if (!wasEngaged) this.#reveal(drag);
496
+ const isCrossing = isOutside !== drag.isOutside;
497
+ if (isCrossing) {
498
+ drag.isOutside = isOutside;
499
+ this.toggleState("cancelling", isOutside);
500
+ }
504
501
  this.input(isOutside ? drag.fromValue : value);
505
- if (isOutside === drag.isOutside) return;
506
- drag.isOutside = isOutside;
507
- this.#renderDragReveal(drag);
502
+ if (isCrossing) this.#renderDragReveal(drag);
508
503
  }
509
504
  #endHolds() {
510
505
  this.#entry?.close(false);
@@ -536,7 +531,9 @@ var SonicValueElement = class extends SonicFormElement {
536
531
  #releaseDrag(drag) {
537
532
  clearTimeout(drag.revealTimer);
538
533
  this.#claim.conceal("drag");
534
+ this.toggleState("cancelling", false);
539
535
  this.#renderHold();
536
+ this.#showRevealed(false);
540
537
  if (this.#value !== drag.fromValue) this.dispatchEvent(new Event("change", { bubbles: true }));
541
538
  this.#springBack();
542
539
  }
@@ -545,59 +542,43 @@ var SonicValueElement = class extends SonicFormElement {
545
542
  if (this.#entry?.isOpen) {
546
543
  for (const name of roleAttributes) control.removeAttribute(name);
547
544
  this.forwardNaming(control, false);
548
- this.#renderReadout();
545
+ this.renderReadout();
549
546
  return;
550
547
  }
551
548
  const [low, high] = this.mapping().bounds;
552
549
  writeAttribute(control, "role", this.controlRole());
553
550
  writeAttribute(control, "aria-valuemin", String(low));
554
551
  writeAttribute(control, "aria-valuemax", String(high));
555
- writeAttribute(control, "aria-valuenow", String(this.#value));
552
+ writeAttribute(control, "aria-valuenow", String(this.#spokenNow()));
556
553
  if (orientation) writeAttribute(control, "aria-orientation", orientation);
557
554
  writeAttribute(control, "aria-valuetext", (this.#formatSpokenValue ?? this.#formatValue)?.(this.#value));
558
555
  this.forwardNaming(control, true);
559
556
  this.#renderDisabled(control);
560
- this.#renderReadout();
557
+ this.renderReadout();
561
558
  }
562
559
  #renderControl() {
563
560
  this.draw();
564
- this.#renderScale();
565
561
  this.toggleState("at-origin", !this.isWrapping() && this.#value === this.mapping().snap(this.originValue()));
566
562
  this.#renderAria(this.focusTarget(), this.controlOrientation());
567
563
  }
568
564
  #renderDisabled(control) {
569
565
  const isDisabled = this.isDisabled();
570
566
  writeAttribute(control, "aria-disabled", isDisabled ? "true" : void 0);
571
- writeAttribute(control, "tabindex", isDisabled ? void 0 : "0");
567
+ const stop = this.getAttribute("tabindex") === "-1" ? "-1" : "0";
568
+ writeAttribute(control, "tabindex", isDisabled ? void 0 : stop);
572
569
  }
573
570
  #renderDragReveal(drag) {
574
- if (drag.isRevealed && !drag.isOutside) this.#claim.reveal("drag");
571
+ const isRevealed = drag.isRevealed && !drag.isOutside;
572
+ if (isRevealed) this.#claim.reveal("drag");
575
573
  else this.#claim.conceal("drag");
576
574
  this.#renderHold();
575
+ this.#showRevealed(isRevealed);
577
576
  }
578
577
  #renderHold() {
579
- this.#renderReadout();
578
+ if (this.heldFrom() === void 0) this.#heldAt = void 0;
579
+ this.renderReadout();
580
580
  this.holdChanged();
581
581
  }
582
- #renderReadout() {
583
- const readout = this.#readout;
584
- if (!readout) return;
585
- const { isOpen, value } = this.#claim.shown(this.#value, this.readout);
586
- readout.bubble.show({
587
- anchor: readout.anchor,
588
- isOpen,
589
- text: value === void 0 ? void 0 : this.#textFor(value)
590
- });
591
- }
592
- #renderScale() {
593
- const marks = this.#scaleMarks?.children ?? [];
594
- const mapping = this.mapping();
595
- for (const mark of marks) {
596
- if (!(mark instanceof HTMLElement || mark instanceof SVGElement)) continue;
597
- const at = String(mapping.proportionOf(scaleValue(mark)));
598
- if (mark.style.getPropertyValue("--_sonic-scale-at") !== at) mark.style.setProperty("--_sonic-scale-at", at);
599
- }
600
- }
601
582
  #reset() {
602
583
  this.#commit(this.default ?? NaN);
603
584
  }
@@ -616,6 +597,28 @@ var SonicValueElement = class extends SonicFormElement {
616
597
  }
617
598
  this.input(next);
618
599
  }
600
+ #shown() {
601
+ if (this.isEditing()) return {
602
+ isOpen: true,
603
+ value: void 0
604
+ };
605
+ const isRevealed = this.#claim.isRevealed;
606
+ const preview = isRevealed ? void 0 : this.previewValue();
607
+ return {
608
+ isOpen: (isRevealed || preview !== void 0) && this.readout,
609
+ value: preview ?? this.#value
610
+ };
611
+ }
612
+ #showRevealed(isRevealed) {
613
+ if (isRevealed === this.#isRevealed) return;
614
+ this.#isRevealed = isRevealed;
615
+ this.toggleState("revealed", isRevealed);
616
+ this.dispatchEvent(new Event("sonic-reveal", { bubbles: true }));
617
+ }
618
+ #spokenNow() {
619
+ const step = this.spokenStep;
620
+ return step !== void 0 && step > 0 ? trimFloat(Math.round(this.#value / step) * step) : this.#value;
621
+ }
619
622
  #springBack() {
620
623
  const target = this.springTarget();
621
624
  if (target === void 0) return;
@@ -645,19 +648,6 @@ var SonicValueElement = class extends SonicFormElement {
645
648
  #textFor(value) {
646
649
  return this.#formatValue?.(value) ?? String(value);
647
650
  }
648
- #watchEscape(isHovered) {
649
- if (!isHovered) {
650
- this.#escapeWatch?.abort();
651
- this.#escapeWatch = void 0;
652
- return;
653
- }
654
- if (this.#escapeWatch) return;
655
- const watch = new AbortController();
656
- this.#escapeWatch = watch;
657
- this.ownerDocument.addEventListener("keydown", (event) => {
658
- if (event.key === "Escape" && this.#claim.dismiss()) this.#renderHold();
659
- }, { signal: watch.signal });
660
- }
661
651
  #withinLimit(mapping, state) {
662
652
  const limit = this.#limit;
663
653
  if (!limit || mapping.isWrapping) return state;