@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
@@ -2,19 +2,44 @@ import { SonicValueElement } from "./value-element.js";
2
2
  import { bindSurface } from "../lib/canvas-surface.js";
3
3
 
4
4
  //#region src/elements/wave-element.ts
5
+ function markerKey(marker) {
6
+ return marker ? [
7
+ marker.start,
8
+ marker.end,
9
+ marker.kind,
10
+ marker.label
11
+ ].join("|") : "";
12
+ }
5
13
  var SonicWaveElement = class extends SonicValueElement {
14
+ static observedAttributes = [...SonicValueElement.observedAttributes, "fill"];
15
+ get currentMarker() {
16
+ return this.#current ? { ...this.#current } : void 0;
17
+ }
18
+ get fill() {
19
+ return this.hasAttribute("fill");
20
+ }
21
+ set fill(isFilling) {
22
+ this.reflect("fill", isFilling);
23
+ }
6
24
  get markers() {
7
25
  return this.#markers.map((marker) => ({ ...marker }));
8
26
  }
9
27
  set markers(list) {
10
28
  this.#markers = [...list ?? []].filter((marker) => Number.isFinite(marker.start)).map((marker) => ({ ...marker })).toSorted((first, second) => first.start - second.start);
11
- if (this.isBound()) this.renderMarkers();
29
+ if (!this.isBound()) return;
30
+ this.renderMarkers();
31
+ this.render();
12
32
  }
13
- #heldAt;
33
+ #current;
34
+ #fillWatch;
14
35
  #markers = [];
15
36
  #surface;
37
+ attributeChangedCallback(name) {
38
+ super.attributeChangedCallback(name);
39
+ if (name === "fill") this.#watchFill();
40
+ }
16
41
  connectedCallback() {
17
- this.upgradeProperties("markers");
42
+ this.upgradeProperties("fill", "markers");
18
43
  super.connectedCallback();
19
44
  }
20
45
  repaint() {
@@ -38,16 +63,20 @@ var SonicWaveElement = class extends SonicValueElement {
38
63
  });
39
64
  this.render();
40
65
  this.checkStyles(control, this.sheet);
66
+ this.#watchFill();
67
+ signal.addEventListener("abort", () => {
68
+ this.#fillWatch?.disconnect();
69
+ this.#fillWatch = void 0;
70
+ }, { once: true });
41
71
  }
42
72
  focusTarget() {
43
73
  return this.control;
44
74
  }
45
75
  heldWrite(next) {
46
- this.#heldAt = next;
76
+ super.heldWrite(next);
47
77
  this.render();
48
78
  }
49
79
  holdChanged() {
50
- if (this.heldFrom() === void 0) this.#heldAt = void 0;
51
80
  this.render();
52
81
  }
53
82
  markerList() {
@@ -61,19 +90,40 @@ var SonicWaveElement = class extends SonicValueElement {
61
90
  return [Math.min(at, to), Math.max(at, to)];
62
91
  });
63
92
  }
64
- playback() {
65
- const from = this.heldFrom();
66
- return from === void 0 ? this.value : this.#heldAt ?? from;
67
- }
68
93
  proportionsChanged() {
69
94
  this.renderMarkers();
70
95
  }
71
96
  renderEmpty() {
72
97
  this.toggleState("empty", this.isEmpty());
73
98
  }
99
+ showCurrentMarker(seconds) {
100
+ const current = this.#markers.findLast((marker) => marker.start <= seconds);
101
+ if (markerKey(current) === markerKey(this.#current)) return;
102
+ this.#current = current;
103
+ this.dispatchEvent(new Event("sonic-marker", { bubbles: true }));
104
+ }
74
105
  surface() {
75
106
  return this.#surface;
76
107
  }
108
+ #watchFill() {
109
+ const control = this.control;
110
+ const isFilling = this.fill && this.isBound();
111
+ if (isFilling === (this.#fillWatch !== void 0)) return;
112
+ this.#fillWatch?.disconnect();
113
+ this.#fillWatch = void 0;
114
+ if (!isFilling) {
115
+ control.style.removeProperty(this.sizeProperty);
116
+ return;
117
+ }
118
+ this.#fillWatch = new ResizeObserver((entries) => {
119
+ const box = entries.at(-1)?.borderBoxSize[0];
120
+ if (!box) return;
121
+ requestAnimationFrame(() => {
122
+ if (this.#fillWatch) control.style.setProperty(this.sizeProperty, `${String(box.blockSize)}px`);
123
+ });
124
+ });
125
+ this.#fillWatch.observe(control);
126
+ }
77
127
  };
78
128
 
79
129
  //#endregion
@@ -1,6 +1,6 @@
1
1
  import { clamp } from "../lib/math.js";
2
2
  import { requireChild, template } from "../lib/render.js";
3
- import { readRegions } from "../lib/time-regions.js";
3
+ import { dueRegions, readRegions } from "../lib/time-regions.js";
4
4
  import { SonicWaveElement } from "./wave-element.js";
5
5
  import { frameTimeline } from "../lib/frame-timeline.js";
6
6
  import { isKind, writeKind } from "../lib/marker-band.js";
@@ -10,9 +10,10 @@ import { waveformBuckets } from "../lib/waveform-buckets.js";
10
10
 
11
11
  //#region src/elements/waveform.ts
12
12
  const colours = {
13
+ ends: "--_sonic-waveform-ends",
13
14
  grid: "--_sonic-waveform-grid",
14
- hatch: "--_sonic-waveform-hatch",
15
15
  marker: "--_sonic-waveform-marker",
16
+ placeholder: "--_sonic-waveform-placeholder",
16
17
  wave: "--_sonic-waveform-wave",
17
18
  waveEdge: "--_sonic-waveform-wave-edge"
18
19
  };
@@ -63,7 +64,7 @@ function paintHatch(context, scene, [from, to]) {
63
64
  context.lineTo(x + height, 0);
64
65
  }
65
66
  context.lineWidth = Math.max(1, Math.round(dpr));
66
- context.strokeStyle = scene.colours.hatch;
67
+ context.strokeStyle = scene.colours.ends;
67
68
  context.stroke();
68
69
  context.restore();
69
70
  }
@@ -83,7 +84,7 @@ function paintPlaceholders(context, scene) {
83
84
  context.lineTo(closing, waveY(closing));
84
85
  }
85
86
  context.lineWidth = Math.max(1, Math.round(dpr));
86
- context.strokeStyle = scene.colours.hatch;
87
+ context.strokeStyle = scene.colours.placeholder;
87
88
  context.stroke();
88
89
  }
89
90
  function waveFill(context, scene) {
@@ -111,8 +112,10 @@ function paintWave(context, scene, peaks) {
111
112
  var SonicWaveform = class extends SonicWaveElement {
112
113
  static observedAttributes = [
113
114
  ...SonicWaveElement.observedAttributes,
115
+ "pending-delay",
114
116
  "playing",
115
117
  "playback-rate",
118
+ "reduced-motion",
116
119
  "zoom"
117
120
  ];
118
121
  get peaks() {
@@ -127,9 +130,21 @@ var SonicWaveform = class extends SonicWaveElement {
127
130
  return this.#pending.map(([start, end]) => [start, end]);
128
131
  }
129
132
  set pending(regions) {
133
+ const listedMs = this.#pendingListedMs;
134
+ const nowMs = performance.now();
130
135
  this.#pending = regions ? readRegions(regions) : [];
136
+ this.#pendingListedMs = new Map(this.#pending.map((region) => {
137
+ const key = region.join(":");
138
+ return [key, listedMs.get(key) ?? nowMs];
139
+ }));
131
140
  this.surface()?.invalidate();
132
141
  }
142
+ get pendingDelay() {
143
+ return Math.max(0, this.numberAttribute("pending-delay", 0));
144
+ }
145
+ set pendingDelay(value) {
146
+ this.reflect("pending-delay", value);
147
+ }
133
148
  get playbackRate() {
134
149
  const playbackRate = this.numberAttribute("playback-rate", 1);
135
150
  return playbackRate > 0 ? playbackRate : 1;
@@ -150,11 +165,18 @@ var SonicWaveform = class extends SonicWaveElement {
150
165
  this.#readTime = read;
151
166
  this.render();
152
167
  }
168
+ get reducedMotion() {
169
+ return this.getAttribute("reduced-motion") === "scroll" ? "scroll" : "page";
170
+ }
171
+ set reducedMotion(motion) {
172
+ this.reflect("reduced-motion", motion);
173
+ }
153
174
  get requestPeaks() {
154
175
  return this.#requestPeaks;
155
176
  }
156
177
  set requestPeaks(request) {
157
178
  this.#requestPeaks = request;
179
+ this.#asked = void 0;
158
180
  this.surface()?.invalidate();
159
181
  }
160
182
  get zoom() {
@@ -169,6 +191,8 @@ var SonicWaveform = class extends SonicWaveElement {
169
191
  colours = colours;
170
192
  lengths = {};
171
193
  sheet = "waveform.css";
194
+ sizeProperty = "--_sonic-waveform-size";
195
+ #asked;
172
196
  #clock = createTrackingClock();
173
197
  #drawn;
174
198
  #ghost = requireChild(this.control, ".sonic-waveform-ghost", HTMLDivElement);
@@ -177,16 +201,18 @@ var SonicWaveform = class extends SonicWaveElement {
177
201
  #paintedKey = "";
178
202
  #peaks;
179
203
  #pending = [];
204
+ #pendingListedMs = /* @__PURE__ */ new Map();
180
205
  #playhead = requireChild(this.control, ".sonic-waveform-playhead", HTMLDivElement);
181
206
  #readTime;
182
207
  #requestPeaks;
183
208
  #rider = createLabelRider(this.control, {
184
209
  className: "sonic-waveform-label",
185
210
  colourProperty: "--_sonic-waveform-marker",
211
+ fadeProperty: "--_sonic-waveform-label-fade",
186
212
  insetProperty: "--_sonic-waveform-label-inset"
187
213
  });
188
214
  connectedCallback() {
189
- this.upgradeProperties("peaks", "pending", "playing", "playbackRate", "readTime", "requestPeaks", "zoom");
215
+ this.upgradeProperties("peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "requestPeaks", "zoom");
190
216
  super.connectedCallback();
191
217
  }
192
218
  connect(signal) {
@@ -204,6 +230,7 @@ var SonicWaveform = class extends SonicWaveElement {
204
230
  }
205
231
  paint(context, { frameMs, isDirty, isRebuilt, look, size }) {
206
232
  const held = this.#held();
233
+ const pending = this.#duePending();
207
234
  const timeline = frameTimeline({
208
235
  clockSeconds: this.#clock.read(frameMs, {
209
236
  isPlaying: this.playing,
@@ -212,9 +239,10 @@ var SonicWaveform = class extends SonicWaveElement {
212
239
  }),
213
240
  frameMs,
214
241
  ...held ? { held } : {},
215
- isPaged: look.isReducedMotion,
242
+ isPaged: look.isReducedMotion && this.reducedMotion === "page",
216
243
  isPlaying: this.playing,
217
- pending: this.#pending,
244
+ isStill: look.isReducedMotion,
245
+ pending,
218
246
  range: [this.min, Math.max(this.min, this.max)],
219
247
  size,
220
248
  zoom: this.zoom
@@ -226,6 +254,7 @@ var SonicWaveform = class extends SonicWaveElement {
226
254
  };
227
255
  placeLine(this.#playhead, timeline.playheadAt);
228
256
  placeLine(this.#ghost, timeline.ghostAt);
257
+ this.showCurrentMarker(seconds);
229
258
  this.toggleState("pending", view.pending.length > 0);
230
259
  this.#rider.place({
231
260
  playheadSeconds: seconds,
@@ -239,7 +268,7 @@ var SonicWaveform = class extends SonicWaveElement {
239
268
  ...view,
240
269
  colours: look.colours
241
270
  }, isRebuilt);
242
- if (timeline.wanted) this.#requestPeaks?.(...timeline.wanted);
271
+ if (timeline.wanted) this.#ask(timeline.wanted);
243
272
  }
244
273
  if (timeline.isMoving) this.surface()?.requestFrame();
245
274
  }
@@ -253,6 +282,17 @@ var SonicWaveform = class extends SonicWaveElement {
253
282
  }]));
254
283
  this.surface()?.rebuild();
255
284
  }
285
+ #ask(wanted) {
286
+ const asked = this.#requestPeaks?.(...wanted);
287
+ if (!(asked instanceof Promise)) return;
288
+ this.#asked = asked;
289
+ this.#repaintAfter(asked);
290
+ }
291
+ #duePending() {
292
+ const due = dueRegions(this.#pending, this.#pendingListedMs, [performance.now(), this.pendingDelay]);
293
+ if (due.length < this.#pending.length) this.surface()?.requestFrame();
294
+ return due;
295
+ }
256
296
  #grab() {
257
297
  const drawn = this.#drawn ?? {
258
298
  seconds: this.value,
@@ -291,7 +331,7 @@ var SonicWaveform = class extends SonicWaveElement {
291
331
  #paintScene(context, scene, isRebuilt) {
292
332
  const { dpr, height, pixelsPerSecond, startSeconds, width } = scene;
293
333
  const x = (seconds) => (seconds - startSeconds) * pixelsPerSecond;
294
- const { grid } = scene.colours;
334
+ const { ends, grid } = scene.colours;
295
335
  const line = Math.max(1, Math.round(dpr));
296
336
  const [opening, closing] = [x(this.min), x(Math.max(this.min, this.max))];
297
337
  const [from, to] = [clamp(opening, 0, width), clamp(closing, 0, width)];
@@ -302,7 +342,7 @@ var SonicWaveform = class extends SonicWaveElement {
302
342
  if (to > from) context.fillRect(from, Math.round((height - line) / 2), to - from, line);
303
343
  paintPlaceholders(context, scene);
304
344
  if (this.#peaks) paintWave(context, scene, this.#peaks);
305
- context.fillStyle = grid;
345
+ context.fillStyle = ends;
306
346
  for (const edge of [opening, closing]) context.fillRect(edge - line / 2, 0, line, height);
307
347
  this.#paintMarkers(context, scene, isRebuilt);
308
348
  }
@@ -319,6 +359,12 @@ var SonicWaveform = class extends SonicWaveElement {
319
359
  for (const probe of probes) probe.remove();
320
360
  return new Map(kinds.map((kind, index) => [kind, read[index] ?? ""]));
321
361
  }
362
+ async #repaintAfter(asked) {
363
+ await Promise.allSettled([asked]);
364
+ if (this.#asked !== asked) return;
365
+ this.#asked = void 0;
366
+ this.surface()?.invalidate();
367
+ }
322
368
  #sourceSeconds() {
323
369
  return this.#readTime?.() ?? this.playback();
324
370
  }
@@ -1,9 +1,11 @@
1
1
  import { clampProportion } from "../lib/math.js";
2
2
  import { requireChild, template } from "../lib/render.js";
3
3
  import { readPxProperty } from "../lib/read-px-property.js";
4
+ import { bindHoverPreview } from "../lib/hover-preview.js";
5
+ import { sortedRegions } from "../lib/time-regions.js";
4
6
  import { SonicWaveElement } from "./wave-element.js";
5
7
  import { createMarkerBand } from "../lib/marker-band.js";
6
- import { stripBars, stripRegions } from "../lib/strip-scene.js";
8
+ import { stripBars, stripGroove, stripRegions } from "../lib/strip-scene.js";
7
9
 
8
10
  //#region src/elements/wavestrip.ts
9
11
  const colours = {
@@ -14,6 +16,7 @@ const colours = {
14
16
  };
15
17
  const lengths = {
16
18
  gapRatio: "--_sonic-wavestrip-bar-gap-ratio",
19
+ grooveRatio: "--_sonic-wavestrip-groove-ratio",
17
20
  pitch: "--_sonic-wavestrip-bar-pitch",
18
21
  radiusRatio: "--_sonic-wavestrip-bar-radius-ratio"
19
22
  };
@@ -45,6 +48,13 @@ function fillRegion(context, path, [from, to, style]) {
45
48
  context.restore();
46
49
  }
47
50
  var SonicWavestrip = class extends SonicWaveElement {
51
+ get buffered() {
52
+ return this.#buffered.map(([start, end]) => [start, end]);
53
+ }
54
+ set buffered(regions) {
55
+ this.#buffered = sortedRegions(regions);
56
+ this.render();
57
+ }
48
58
  get cancellable() {
49
59
  return this.hasAttribute("cancellable");
50
60
  }
@@ -64,22 +74,43 @@ var SonicWavestrip = class extends SonicWaveElement {
64
74
  colours = colours;
65
75
  lengths = lengths;
66
76
  sheet = "wavestrip.css";
77
+ sizeProperty = "--_sonic-wavestrip-size";
67
78
  #bars;
79
+ #buffered = [];
80
+ #hover;
68
81
  #markerBand = createMarkerBand(requireChild(this.control, ".sonic-wavestrip-markers", HTMLDivElement));
69
82
  #paintedKey = "";
70
83
  #peaks;
71
84
  connectedCallback() {
72
- this.upgradeProperties("cancellable", "peaks");
85
+ this.upgradeProperties("buffered", "cancellable", "peaks");
73
86
  super.connectedCallback();
74
87
  }
75
88
  connect(signal) {
76
89
  super.connect(signal);
77
- this.#bindHover(this.control, signal);
90
+ this.#hover = bindHoverPreview(this.control, {
91
+ canShow: () => this.readout,
92
+ dismiss: () => {
93
+ this.render();
94
+ },
95
+ isTaken: () => this.isEditing() || this.isRevealed(),
96
+ show: () => {
97
+ this.renderReadout();
98
+ this.#placeReadout();
99
+ },
100
+ valueAt: (event) => {
101
+ const axis = this.#axis();
102
+ const at = clampProportion((event.clientX - axis.startPx) / axis.travelPx);
103
+ const mapping = this.mapping();
104
+ return this.#markerAt(event, axis) ?? mapping.snap(mapping.valueAt(at));
105
+ }
106
+ }, signal);
78
107
  this.bindGestures(this.control, signal, (event) => this.#grab(event));
79
108
  }
80
109
  draw() {
81
110
  const surface = this.surface();
111
+ if (this.isEditing() || this.isRevealed()) this.#hover?.clear();
82
112
  this.#placeReadout();
113
+ this.showCurrentMarker(this.playback());
83
114
  if (surface && this.#stripRegions(surface.size.width).key !== this.#paintedKey) surface.requestFrame();
84
115
  }
85
116
  isEmpty() {
@@ -100,6 +131,9 @@ var SonicWavestrip = class extends SonicWaveElement {
100
131
  look.colours[kind]
101
132
  ]);
102
133
  }
134
+ previewValue() {
135
+ return this.#hover?.value();
136
+ }
103
137
  renderMarkers() {
104
138
  this.#markerBand.render(this.markerList(), this.markerProportions());
105
139
  }
@@ -116,28 +150,18 @@ var SonicWavestrip = class extends SonicWaveElement {
116
150
  };
117
151
  }
118
152
  #barsFor({ isDirty, look, size }) {
119
- const peaks = this.#peaks;
120
- if (!peaks || peaks.length === 0) return void 0;
121
153
  if (!isDirty && this.#bars) return this.#bars;
154
+ const peaks = this.#peaks;
155
+ const groove = stripGroove(size, look.lengths.grooveRatio);
156
+ const drawn = groove ? [groove] : [];
157
+ const bars = peaks && peaks.length > 0 ? stripBars(peaks, size, look.lengths) : drawn;
158
+ this.#bars = void 0;
159
+ if (bars.length === 0) return void 0;
122
160
  const path = new Path2D();
123
- for (const { height, radius, width, x, y } of stripBars(peaks, size, look.lengths)) path.roundRect(x, y, width, height, radius);
161
+ for (const { height, radius, width, x, y } of bars) path.roundRect(x, y, width, height, radius);
124
162
  this.#bars = path;
125
163
  return path;
126
164
  }
127
- #bindHover(strip, signal) {
128
- strip.addEventListener("pointermove", (event) => {
129
- if (event.pointerType === "touch" || event.buttons !== 0) return;
130
- if (!this.readout) return;
131
- const axis = this.#axis();
132
- const at = clampProportion((event.clientX - axis.startPx) / axis.travelPx);
133
- const mapping = this.mapping();
134
- this.hoverReadout(this.#markerAt(event, axis) ?? mapping.snap(mapping.valueAt(at)));
135
- this.#placeReadout();
136
- }, { signal });
137
- for (const type of ["pointerdown", "pointerleave"]) strip.addEventListener(type, () => {
138
- this.hoverReadout(void 0);
139
- }, { signal });
140
- }
141
165
  #grab(event) {
142
166
  const axis = this.#axis();
143
167
  const at = (event.clientX - axis.startPx) / axis.travelPx;
@@ -174,7 +198,7 @@ var SonicWavestrip = class extends SonicWaveElement {
174
198
  const { proportionOf } = this.mapping();
175
199
  const isScrubbing = this.heldFrom() !== void 0 && this.isRevealed();
176
200
  return stripRegions({
177
- buffered: this.buffered.map(([start, end]) => [proportionOf(start), proportionOf(end)]),
201
+ buffered: this.#buffered.map(([start, end]) => [proportionOf(start), proportionOf(end)]),
178
202
  played: proportionOf(this.playback()),
179
203
  ...isScrubbing ? { scrub: proportionOf(this.value) } : {}
180
204
  }, width);
@@ -1,13 +1,13 @@
1
1
  import { clampProportion, toNumber } from "../lib/math.js";
2
- import { requireChild, template } from "../lib/render.js";
3
2
  import { writeAttribute } from "../lib/write-attribute.js";
4
3
  import { SonicFormElement } from "./form-element.js";
4
+ import { requireChild, template } from "../lib/render.js";
5
+ import { bindDrag } from "../lib/pointer-drag.js";
5
6
  import { ReadoutClaim } from "./readout-claim.js";
6
7
  import { Readout } from "./readout.js";
7
8
  import { dragThresholdPx } from "../lib/drag-step.js";
8
9
  import { focusByPointer } from "../lib/focus-by-pointer.js";
9
10
  import { isMenuPress, isResetPress } from "../lib/modifier-press.js";
10
- import { bindDrag } from "../lib/pointer-drag.js";
11
11
  import { resetKeys, valueMapping } from "../lib/value-mapping.js";
12
12
  import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
13
13
 
@@ -29,6 +29,7 @@ const orientations = {
29
29
  };
30
30
  const renderXy = template(`
31
31
  <div class="sonic-xy">
32
+ <div class="sonic-xy-bracket" aria-hidden="true"></div>
32
33
  <div class="sonic-xy-field" aria-hidden="true"></div>
33
34
  <div class="sonic-xy-puck">
34
35
  <div class="sonic-xy-axis" data-sonic-axis="x" role="slider" tabindex="0"></div>
@@ -6,6 +6,7 @@ import { SonicMeter } from "./elements/meter.js";
6
6
  import { SonicNumber } from "./elements/number.js";
7
7
  import { SonicSegmented } from "./elements/segmented.js";
8
8
  import { SonicSlider } from "./elements/slider.js";
9
+ import { SonicSpectrum } from "./elements/spectrum.js";
9
10
  import { SonicSplit } from "./elements/split.js";
10
11
  import { SonicSwitch } from "./elements/switch.js";
11
12
  import { SonicWaveform } from "./elements/waveform.js";
@@ -14,4 +15,4 @@ import { SonicXy } from "./elements/xy.js";
14
15
  import { crossfadeGains } from "./lib/crossfade.js";
15
16
  import { formatPercent, parsePercent } from "./lib/percent.js";
16
17
 
17
- export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
18
+ export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatPercent, parsePercent };
@@ -63,8 +63,10 @@ var CanvasSurface = class {
63
63
  this.#schedule();
64
64
  }
65
65
  #armTransitions() {
66
- const { style } = this.#options.canvas;
67
- style.setProperty("transition-property", Object.values(this.#options.colours).join(", "));
66
+ const { canvas, colours, numbers = {} } = this.#options;
67
+ const { style } = canvas;
68
+ const watched = [...Object.values(colours), ...Object.values(numbers)];
69
+ style.setProperty("transition-property", watched.join(", "));
68
70
  style.setProperty("transition-duration", "0.001ms");
69
71
  style.setProperty("transition-timing-function", "step-start");
70
72
  style.setProperty("transition-behavior", "allow-discrete");
@@ -148,12 +150,13 @@ var CanvasSurface = class {
148
150
  }, { once: true });
149
151
  }
150
152
  #read() {
151
- const { canvas, colours, lengths = {} } = this.#options;
153
+ const { canvas, colours, lengths = {}, numbers = {} } = this.#options;
152
154
  const styles = getComputedStyle(canvas);
153
155
  return {
154
156
  colours: readEach(colours, (property) => styles.getPropertyValue(property).trim().replaceAll("currentcolor", () => styles.color)),
155
157
  isReducedMotion: this.#reducedMotion.matches,
156
- lengths: readEach(lengths, (property) => readPxProperty(styles, property, 0))
158
+ lengths: readEach(lengths, (property) => readPxProperty(styles, property, 0)),
159
+ numbers: readEach(numbers, (property) => readPxProperty(styles, property, 0))
157
160
  };
158
161
  }
159
162
  #rescale() {
@@ -20,8 +20,8 @@ function pendingIn({ pending, range }, window) {
20
20
  return shown ? [shown] : [];
21
21
  });
22
22
  }
23
- function placeholderPhase({ frameMs, isPaged }, pending) {
24
- return !isPaged && pending.length > 0 ? frameMs / placeholderPeriodMs : void 0;
23
+ function placeholderPhase({ frameMs, isStill }, pending) {
24
+ return !isStill && pending.length > 0 ? frameMs / placeholderPeriodMs : void 0;
25
25
  }
26
26
  function frameTimeline(input) {
27
27
  const { clockSeconds, held, range, size } = input;
@@ -0,0 +1,46 @@
1
+ //#region src/lib/hover-preview.ts
2
+ function bindHoverPreview(target, options, signal) {
3
+ let hover;
4
+ let isDismissed = false;
5
+ let escapeWatch;
6
+ const watchEscape = (isHovered) => {
7
+ if (!isHovered) {
8
+ escapeWatch?.abort();
9
+ escapeWatch = void 0;
10
+ return;
11
+ }
12
+ if (escapeWatch) return;
13
+ escapeWatch = new AbortController();
14
+ target.ownerDocument.addEventListener("keydown", (event) => {
15
+ if (hover === void 0 || event.key !== "Escape") return;
16
+ hover = void 0;
17
+ isDismissed = true;
18
+ options.dismiss();
19
+ }, { signal: escapeWatch.signal });
20
+ };
21
+ const hoverAt = (value) => {
22
+ if (value === void 0) isDismissed = false;
23
+ if (!isDismissed && !options.isTaken()) hover = value;
24
+ options.show();
25
+ watchEscape(value !== void 0);
26
+ };
27
+ target.addEventListener("pointermove", (event) => {
28
+ if (event.pointerType === "touch" || event.buttons !== 0 || !options.canShow()) return;
29
+ hoverAt(options.valueAt(event));
30
+ }, { signal });
31
+ for (const type of ["pointerdown", "pointerleave"]) target.addEventListener(type, () => {
32
+ hoverAt(void 0);
33
+ }, { signal });
34
+ signal.addEventListener("abort", () => {
35
+ watchEscape(false);
36
+ }, { once: true });
37
+ return {
38
+ clear: () => {
39
+ hover = void 0;
40
+ },
41
+ value: () => hover
42
+ };
43
+ }
44
+
45
+ //#endregion
46
+ export { bindHoverPreview };
@@ -4,7 +4,6 @@ import { readPxProperty } from "./read-px-property.js";
4
4
  import { writeKind } from "./marker-band.js";
5
5
 
6
6
  //#region src/lib/marker-rider.ts
7
- const fadeShare = .25;
8
7
  function lineX(seconds, view) {
9
8
  return (seconds - view.startSeconds) / view.windowSeconds * view.widthPx;
10
9
  }
@@ -16,7 +15,7 @@ function parkedPlacement(labels, index, [view, arrivingX]) {
16
15
  const approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;
17
16
  return {
18
17
  index,
19
- opacity: clamp(approachPx / (view.widthPx * fadeShare), 0, 1),
18
+ opacity: view.fadeShare > 0 ? clamp(approachPx / (view.widthPx * view.fadeShare), 0, 1) : 1,
20
19
  shownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),
21
20
  x
22
21
  };
@@ -91,15 +90,18 @@ function measureWidths(control, className, texts) {
91
90
  return widths;
92
91
  }
93
92
  function createLabelRider(control, options) {
94
- const { className, colourProperty, insetProperty } = options;
93
+ const { className, colourProperty, fadeProperty, insetProperty } = options;
95
94
  const parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));
96
95
  const arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));
96
+ let fadeShare = .25;
97
97
  let insetPx = 0;
98
98
  let measured = [];
99
99
  return {
100
100
  measure: (labels) => {
101
101
  const widths = measureWidths(control, className, labels.map(({ text }) => text));
102
- insetPx = readPxProperty(getComputedStyle(control), insetProperty, 0);
102
+ const styles = getComputedStyle(control);
103
+ fadeShare = readPxProperty(styles, fadeProperty, .25);
104
+ insetPx = readPxProperty(styles, insetProperty, 0);
103
105
  measured = labels.map((label, index) => ({
104
106
  ...label,
105
107
  widthPx: widths[index] ?? 0
@@ -108,6 +110,7 @@ function createLabelRider(control, options) {
108
110
  place: (window) => {
109
111
  const layout = layoutRider(measured, {
110
112
  ...window,
113
+ fadeShare,
111
114
  insetPx
112
115
  });
113
116
  writeLabel(parked, [measured, colourProperty], layout.parked);
@@ -0,0 +1,64 @@
1
+ import { appendOnce } from "./owned-control.js";
2
+ import { attachSlots } from "./render.js";
3
+
4
+ //#region src/lib/mirror-children.ts
5
+ const hostSlots = /* @__PURE__ */ new WeakMap();
6
+ function childHolding(host, node) {
7
+ let current = node;
8
+ while (current && current.parentNode !== host) current = current.parentNode;
9
+ return current ?? void 0;
10
+ }
11
+ function isCopiedChange(host, record, { control, isCopied }) {
12
+ if (record.target === host) return record.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control);
13
+ const child = childHolding(host, record.target);
14
+ if (child === void 0) return false;
15
+ return isCopied(child) || record.type === "attributes" && record.target === child;
16
+ }
17
+ function mirrorChildren(host, { control, copy, isCopied = () => true, isPassed = () => false, place }, signal) {
18
+ const slots = hostSlots.get(host) ?? attachSlots(host);
19
+ let copies = /* @__PURE__ */ new Map();
20
+ hostSlots.set(host, slots);
21
+ const mirror = (touched) => {
22
+ appendOnce(host, control);
23
+ const children = [...host.childNodes].filter((child) => child !== control);
24
+ const slotted = children.filter((child) => child instanceof Element || child instanceof Text);
25
+ const kept = copies;
26
+ const originals = children.filter((child) => isCopied(child));
27
+ copies = new Map(originals.map((original) => [original, touched && !touched.has(original) && kept.has(original) ? kept.get(original) : copy(original)]));
28
+ place([...copies.values()].filter((made) => made !== void 0));
29
+ slots.shown.assign(...slotted.filter((child) => !isCopied(child)), control);
30
+ slots.kept.assign(...slotted.filter((child) => isCopied(child)));
31
+ };
32
+ const watch = () => {
33
+ observer.disconnect();
34
+ observer.observe(host, { childList: true });
35
+ for (const child of host.childNodes) {
36
+ if (child === control) continue;
37
+ observer.observe(child, {
38
+ attributes: true,
39
+ characterData: true,
40
+ childList: true,
41
+ subtree: true
42
+ });
43
+ }
44
+ };
45
+ const observer = new MutationObserver((records) => {
46
+ const changes = records.filter((record) => isCopiedChange(host, record, {
47
+ control,
48
+ isCopied
49
+ }));
50
+ if (changes.length === 0) return;
51
+ if (changes.some((record) => record.target === host)) {
52
+ mirror();
53
+ watch();
54
+ } else mirror(new Set(changes.map((record) => childHolding(host, record.target))));
55
+ });
56
+ mirror();
57
+ watch();
58
+ signal.addEventListener("abort", () => {
59
+ observer.disconnect();
60
+ }, { once: true });
61
+ }
62
+
63
+ //#endregion
64
+ export { mirrorChildren };