@xsynaptic/sonic-ui 0.6.1 → 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 (84) 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 +30 -2
  5. package/dist/default/elements/display-element.js +77 -0
  6. package/dist/default/elements/envelope.js +1 -1
  7. package/dist/default/elements/slider.js +67 -1
  8. package/dist/default/elements/spectrum.js +230 -0
  9. package/dist/default/elements/value-element.js +49 -11
  10. package/dist/default/elements/wave-element.js +59 -9
  11. package/dist/default/elements/waveform.js +56 -10
  12. package/dist/default/elements/wavestrip.js +31 -45
  13. package/dist/default/elements/xy.js +1 -1
  14. package/dist/default/index.js +2 -1
  15. package/dist/default/lib/canvas-surface.js +7 -4
  16. package/dist/default/lib/frame-timeline.js +2 -2
  17. package/dist/default/lib/hover-preview.js +46 -0
  18. package/dist/default/lib/marker-rider.js +7 -4
  19. package/dist/default/lib/pointer-drag.js +7 -2
  20. package/dist/default/lib/spectrum-columns.js +74 -0
  21. package/dist/default/lib/spectrum-levels.js +55 -0
  22. package/dist/default/lib/strip-scene.js +12 -1
  23. package/dist/default/lib/time-regions.js +5 -1
  24. package/dist/dev/define/spectrum.d.ts +2 -0
  25. package/dist/dev/define/spectrum.js +8 -0
  26. package/dist/dev/define/spectrum.js.map +1 -0
  27. package/dist/dev/define.d.ts +3 -1
  28. package/dist/dev/define.js +1 -0
  29. package/dist/dev/elements/button.d.ts +2 -0
  30. package/dist/dev/elements/button.js +30 -2
  31. package/dist/dev/elements/button.js.map +1 -1
  32. package/dist/dev/elements/display-element.d.ts +29 -0
  33. package/dist/dev/elements/display-element.js +78 -0
  34. package/dist/dev/elements/display-element.js.map +1 -0
  35. package/dist/dev/elements/envelope.js +1 -1
  36. package/dist/dev/elements/slider.d.ts +6 -0
  37. package/dist/dev/elements/slider.js +67 -1
  38. package/dist/dev/elements/slider.js.map +1 -1
  39. package/dist/dev/elements/spectrum.d.ts +59 -0
  40. package/dist/dev/elements/spectrum.js +231 -0
  41. package/dist/dev/elements/spectrum.js.map +1 -0
  42. package/dist/dev/elements/value-element.d.ts +5 -1
  43. package/dist/dev/elements/value-element.js +49 -11
  44. package/dist/dev/elements/value-element.js.map +1 -1
  45. package/dist/dev/elements/wave-element.d.ts +8 -2
  46. package/dist/dev/elements/wave-element.js +59 -9
  47. package/dist/dev/elements/wave-element.js.map +1 -1
  48. package/dist/dev/elements/waveform.d.ts +12 -4
  49. package/dist/dev/elements/waveform.js +56 -10
  50. package/dist/dev/elements/waveform.js.map +1 -1
  51. package/dist/dev/elements/wavestrip.d.ts +3 -0
  52. package/dist/dev/elements/wavestrip.js +31 -45
  53. package/dist/dev/elements/wavestrip.js.map +1 -1
  54. package/dist/dev/elements/xy.js +1 -1
  55. package/dist/dev/index.d.ts +4 -1
  56. package/dist/dev/index.js +2 -1
  57. package/dist/dev/lib/canvas-surface.d.ts +4 -3
  58. package/dist/dev/lib/canvas-surface.js +7 -4
  59. package/dist/dev/lib/canvas-surface.js.map +1 -1
  60. package/dist/dev/lib/frame-timeline.js +2 -2
  61. package/dist/dev/lib/frame-timeline.js.map +1 -1
  62. package/dist/dev/lib/hover-preview.js +47 -0
  63. package/dist/dev/lib/hover-preview.js.map +1 -0
  64. package/dist/dev/lib/marker-rider.js +7 -4
  65. package/dist/dev/lib/marker-rider.js.map +1 -1
  66. package/dist/dev/lib/pointer-drag.js +7 -2
  67. package/dist/dev/lib/pointer-drag.js.map +1 -1
  68. package/dist/dev/lib/spectrum-columns.js +75 -0
  69. package/dist/dev/lib/spectrum-columns.js.map +1 -0
  70. package/dist/dev/lib/spectrum-levels.js +56 -0
  71. package/dist/dev/lib/spectrum-levels.js.map +1 -0
  72. package/dist/dev/lib/strip-scene.js +12 -1
  73. package/dist/dev/lib/strip-scene.js.map +1 -1
  74. package/dist/dev/lib/time-regions.js +5 -1
  75. package/dist/dev/lib/time-regions.js.map +1 -1
  76. package/dist/styles/button.css +7 -1
  77. package/dist/styles/controls.css +1 -0
  78. package/dist/styles/material.css +18 -3
  79. package/dist/styles/meter.css +22 -18
  80. package/dist/styles/slider.css +42 -5
  81. package/dist/styles/spectrum.css +105 -0
  82. package/dist/styles/waveform.css +38 -16
  83. package/dist/styles/wavestrip.css +22 -4
  84. package/package.json +2 -1
@@ -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);
@@ -1,4 +1,9 @@
1
1
  //#region src/lib/pointer-drag.ts
2
+ function capturePointer(target, pointerId) {
3
+ try {
4
+ target.setPointerCapture(pointerId);
5
+ } catch {}
6
+ }
2
7
  function bindDrag(target, handlers, signal) {
3
8
  let held;
4
9
  const end = () => {
@@ -22,8 +27,8 @@ function bindDrag(target, handlers, signal) {
22
27
  drag,
23
28
  pointerId: event.pointerId
24
29
  };
25
- target.setPointerCapture(event.pointerId);
26
30
  handlers.toggle?.(true);
31
+ capturePointer(target, event.pointerId);
27
32
  }, { signal });
28
33
  whileHeld("pointermove", handlers.move);
29
34
  whileHeld("pointercancel", (drag) => {
@@ -43,4 +48,4 @@ function bindDrag(target, handlers, signal) {
43
48
  }
44
49
 
45
50
  //#endregion
46
- export { bindDrag };
51
+ export { bindDrag, capturePointer };
@@ -0,0 +1,74 @@
1
+ import { linearTaper, logTaper } from "./taper.js";
2
+
3
+ //#region src/lib/spectrum-columns.ts
4
+ const gridDecibels = 12;
5
+ function binHertz({ binCount, sampleRate }) {
6
+ return sampleRate / 2 / binCount;
7
+ }
8
+ function columnEdges(axis, width) {
9
+ const edges = new Float64Array(width + 1);
10
+ const taper = logTaper(axis.frequencyMin, axis.frequencyMax);
11
+ if (!taper) return edges;
12
+ const hertz = binHertz(axis);
13
+ for (let edge = 0; edge <= width; edge += 1) edges[edge] = taper.valueAt(edge / width) / hertz;
14
+ return edges;
15
+ }
16
+ function columnLevels(columns, levels, { edges, floor }) {
17
+ const levelAt = (bin) => bin < levels.length ? levels[bin] ?? floor : floor;
18
+ for (let column = 0; column < columns.length; column += 1) {
19
+ const from = edges[column] ?? 0;
20
+ const to = edges[column + 1] ?? from;
21
+ const first = Math.ceil(from);
22
+ const last = Math.ceil(to) - 1;
23
+ if (first > last) {
24
+ const below = levelAt(first - 1);
25
+ columns[column] = below + (levelAt(first) - below) * ((from + to) / 2 - (first - 1));
26
+ continue;
27
+ }
28
+ let loudest = floor;
29
+ for (let bin = first; bin <= last && bin < levels.length; bin += 1) loudest = Math.max(loudest, levelAt(bin));
30
+ columns[column] = loudest;
31
+ }
32
+ }
33
+ function levelRows({ max, min }, height) {
34
+ const taper = linearTaper(min, max);
35
+ return (level) => Math.round((1 - taper.proportionOf(level)) * height);
36
+ }
37
+ function gridColumns({ frequencyMax, frequencyMin }, width) {
38
+ const taper = logTaper(frequencyMin, frequencyMax);
39
+ const columns = [];
40
+ if (!taper) return columns;
41
+ for (let exponent = Math.floor(Math.log10(frequencyMin)) + 1; 10 ** exponent < frequencyMax; exponent += 1) columns.push(Math.round(taper.proportionOf(10 ** exponent) * width));
42
+ return columns;
43
+ }
44
+ function gridRows({ max, min }, rowOf) {
45
+ const rows = [];
46
+ for (let level = (Math.floor(min / gridDecibels) + 1) * gridDecibels; level < max; level += gridDecibels) rows.push(rowOf(level));
47
+ return rows;
48
+ }
49
+ function zoneRows({ clipFrom, hotFrom }, rowOf) {
50
+ const clip = rowOf(clipFrom);
51
+ return {
52
+ clip,
53
+ hot: Math.max(clip, rowOf(hotFrom))
54
+ };
55
+ }
56
+ function loudestBin(frame, axis) {
57
+ const hertz = binHertz(axis);
58
+ const last = Math.min(Math.floor(axis.frequencyMax / hertz), frame.length - 1);
59
+ let loudest = -1;
60
+ let decibels = -Infinity;
61
+ for (let bin = Math.ceil(axis.frequencyMin / hertz); bin <= last; bin += 1) {
62
+ const level = frame[bin] ?? NaN;
63
+ if (!(Number.isFinite(level) && level > decibels)) continue;
64
+ loudest = bin;
65
+ decibels = level;
66
+ }
67
+ return loudest === -1 ? void 0 : {
68
+ decibels,
69
+ frequency: loudest * hertz
70
+ };
71
+ }
72
+
73
+ //#endregion
74
+ export { columnEdges, columnLevels, gridColumns, gridRows, levelRows, loudestBin, zoneRows };
@@ -0,0 +1,55 @@
1
+ import { ballisticsRates, fall } from "./ballistics.js";
2
+
3
+ //#region src/lib/spectrum-levels.ts
4
+ const holdMs = 1500;
5
+ function createLevels(binCount) {
6
+ return {
7
+ bars: new Float64Array(binCount).fill(-Infinity),
8
+ heldAt: new Float64Array(binCount).fill(-Infinity),
9
+ holds: new Float64Array(binCount).fill(-Infinity),
10
+ incoming: new Float64Array(binCount).fill(-Infinity)
11
+ };
12
+ }
13
+ function receiveLevels({ incoming }, frame, floor) {
14
+ for (let bin = 0; bin < incoming.length; bin += 1) {
15
+ const level = frame[bin] ?? NaN;
16
+ incoming[bin] = Math.max(incoming[bin] ?? floor, Number.isFinite(level) ? level : floor);
17
+ }
18
+ }
19
+ function fallenHold(hold, bar, { elapsedMs, isHoldStepped, pastHoldMs }) {
20
+ if (pastHoldMs < 0) return hold;
21
+ if (isHoldStepped) return bar;
22
+ return fall(hold, {
23
+ fallDecibelsPerSecond: ballisticsRates.peak.fallDecibelsPerSecond,
24
+ floor: bar
25
+ }, Math.min(elapsedMs, pastHoldMs));
26
+ }
27
+ function stepLevels(levels, step) {
28
+ const { bars, heldAt, holds, incoming } = levels;
29
+ const rate = {
30
+ fallDecibelsPerSecond: ballisticsRates.peak.fallDecibelsPerSecond,
31
+ floor: step.floor
32
+ };
33
+ for (let bin = 0; bin < bars.length; bin += 1) {
34
+ const bar = Math.max(fall(bars[bin] ?? step.floor, rate, step.elapsedMs), incoming[bin] ?? step.floor);
35
+ const hold = holds[bin] ?? bar;
36
+ bars[bin] = bar;
37
+ incoming[bin] = -Infinity;
38
+ if (!step.isHeld || bar >= hold) {
39
+ holds[bin] = bar;
40
+ heldAt[bin] = step.nowMs;
41
+ continue;
42
+ }
43
+ const pastHoldMs = step.nowMs - (heldAt[bin] ?? step.nowMs) - holdMs;
44
+ holds[bin] = fallenHold(hold, bar, {
45
+ ...step,
46
+ pastHoldMs
47
+ });
48
+ }
49
+ }
50
+ function areSettled({ holds }, floor) {
51
+ return holds.every((hold) => hold <= floor);
52
+ }
53
+
54
+ //#endregion
55
+ export { areSettled, createLevels, receiveLevels, stepLevels };
@@ -18,6 +18,17 @@ function stripBars(peaks, size, grid) {
18
18
  };
19
19
  });
20
20
  }
21
+ function stripGroove(size, ratio) {
22
+ if (ratio <= 0) return void 0;
23
+ const height = Math.max(1, Math.round(size.height * ratio));
24
+ return {
25
+ height,
26
+ radius: height / 2,
27
+ width: size.width,
28
+ x: 0,
29
+ y: Math.round((size.height - height) / 2)
30
+ };
31
+ }
21
32
  function stripRegions(state, widthPx) {
22
33
  const px = (proportion) => Math.round((Number.isFinite(proportion) ? proportion : 0) * widthPx);
23
34
  const buffered = state.buffered.map(([start, end]) => [px(start), px(end)]).filter(([start, end]) => end > start);
@@ -45,4 +56,4 @@ function stripRegions(state, widthPx) {
45
56
  }
46
57
 
47
58
  //#endregion
48
- export { stripBars, stripRegions };
59
+ export { stripBars, stripGroove, stripRegions };
@@ -3,9 +3,13 @@ function readRegions(regions) {
3
3
  if ("start" in regions) return Array.from({ length: regions.length }, (_entry, index) => [regions.start(index), regions.end(index)]);
4
4
  return [...regions].map(([start, end]) => [start, end]);
5
5
  }
6
+ function dueRegions(regions, listedMs, [nowMs, delayMs]) {
7
+ if (delayMs <= 0) return [...regions];
8
+ return regions.filter((region) => nowMs - (listedMs.get(region.join(":")) ?? nowMs) >= delayMs);
9
+ }
6
10
  function sortedRegions(regions) {
7
11
  return regions ? readRegions(regions).toSorted((first, second) => first[0] - second[0]) : [];
8
12
  }
9
13
 
10
14
  //#endregion
11
- export { readRegions, sortedRegions };
15
+ export { dueRegions, readRegions, sortedRegions };
@@ -0,0 +1,2 @@
1
+ import { SonicSpectrum } from "../elements/spectrum.js";
2
+ export type { SonicSpectrum };
@@ -0,0 +1,8 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicSpectrum } from "../elements/spectrum.js";
3
+
4
+ //#region src/define/spectrum.ts
5
+ defineOnce("sonic-spectrum", SonicSpectrum);
6
+
7
+ //#endregion
8
+ //# sourceMappingURL=spectrum.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spectrum.js","names":[],"sources":["../../../src/define/spectrum.ts"],"sourcesContent":["import { defineOnce } from '#elements/define-once.ts';\nimport { SonicSpectrum } from '#elements/spectrum.ts';\n\ndefineOnce('sonic-spectrum', SonicSpectrum);\n\nexport type { SonicSpectrum } from '#elements/spectrum.ts';\n"],"mappings":";;;;AAGA,WAAW,kBAAkB,aAAa"}
@@ -12,6 +12,8 @@ import { SonicSegmented } from "./elements/segmented.js";
12
12
  import "./define/segmented.js";
13
13
  import { SonicSlider } from "./elements/slider.js";
14
14
  import "./define/slider.js";
15
+ import { SonicSpectrum } from "./elements/spectrum.js";
16
+ import "./define/spectrum.js";
15
17
  import { SonicSplit } from "./elements/split.js";
16
18
  import "./define/split.js";
17
19
  import { SonicSwitch } from "./elements/switch.js";
@@ -22,4 +24,4 @@ import { SonicWavestrip } from "./elements/wavestrip.js";
22
24
  import "./define/wavestrip.js";
23
25
  import { SonicXy } from "./elements/xy.js";
24
26
  import "./define/xy.js";
25
- export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy };
27
+ export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicWaveform, SonicWavestrip, SonicXy };
@@ -5,6 +5,7 @@ import "./define/meter.js";
5
5
  import "./define/number.js";
6
6
  import "./define/segmented.js";
7
7
  import "./define/slider.js";
8
+ import "./define/spectrum.js";
8
9
  import "./define/split.js";
9
10
  import "./define/switch.js";
10
11
  import "./define/waveform.js";
@@ -16,6 +16,8 @@ export declare class SonicButton extends SonicFormElement {
16
16
  set defaultPressed(isPressed: boolean);
17
17
  get latching(): boolean;
18
18
  set latching(isLatching: boolean);
19
+ get legend(): string | undefined;
20
+ set legend(name: string | undefined);
19
21
  get momentary(): boolean;
20
22
  set momentary(isMomentary: boolean);
21
23
  get pressed(): boolean;
@@ -4,16 +4,25 @@ import { copyNode } from "../lib/copy-node.js";
4
4
  import { bindHold } from "../lib/hold.js";
5
5
  import { placeChildren, requireChild, template } from "../lib/render.js";
6
6
  import { mirrorChildren } from "../lib/mirror-children.js";
7
+ import { capturePointer } from "../lib/pointer-drag.js";
7
8
 
8
9
  //#region src/elements/button.ts
9
10
  const renderButton = template(`
10
11
  <button class="sonic-button" type="button"><span class="sonic-button-cap"></span></button>
11
12
  `, HTMLButtonElement);
12
13
  const holdKeys = /* @__PURE__ */ new Set([" ", "Enter"]);
14
+ const popupAttributes = {
15
+ controls: "aria-controls",
16
+ expanded: "aria-expanded",
17
+ popup: "aria-haspopup"
18
+ };
19
+ const pressWhens = /* @__PURE__ */ new Set(["pressed", "released"]);
13
20
  var SonicButton = class extends SonicFormElement {
14
21
  static observedAttributes = [
15
22
  ...SonicFormElement.observedAttributes,
23
+ ...Object.keys(popupAttributes),
16
24
  "busy",
25
+ "legend",
17
26
  "momentary",
18
27
  "pressed",
19
28
  "soft-disabled",
@@ -44,6 +53,12 @@ var SonicButton = class extends SonicFormElement {
44
53
  set latching(isLatching) {
45
54
  this.reflect("latching", isLatching);
46
55
  }
56
+ get legend() {
57
+ return this.getAttribute("legend") ?? void 0;
58
+ }
59
+ set legend(name) {
60
+ this.reflect("legend", name);
61
+ }
47
62
  get momentary() {
48
63
  return this.hasAttribute("momentary");
49
64
  }
@@ -88,12 +103,13 @@ var SonicButton = class extends SonicFormElement {
88
103
  connect(signal) {
89
104
  const button = this.#button;
90
105
  const cap = requireChild(button, ".sonic-button-cap", HTMLSpanElement);
91
- this.upgradeProperties("armed", "busy", "defaultPressed", "momentary", "softDisabled", "latching", "pressed", "value");
106
+ this.upgradeProperties("armed", "busy", "defaultPressed", "legend", "momentary", "softDisabled", "latching", "pressed", "value");
92
107
  mirrorChildren(this, {
93
108
  control: button,
94
109
  copy: copyNode,
95
110
  place: (copies) => {
96
111
  placeChildren(cap, copies);
112
+ this.#showLegend();
97
113
  }
98
114
  }, signal);
99
115
  this.render();
@@ -122,6 +138,8 @@ var SonicButton = class extends SonicFormElement {
122
138
  writeAttribute(button, "aria-pressed", this.latching ? String(this.pressed) : void 0);
123
139
  this.toggleState("pressed", this.pressed);
124
140
  this.forwardNaming(button, true);
141
+ this.#forwardPopup(button);
142
+ this.#showLegend();
125
143
  const submitted = this.latching && this.pressed ? this.value : null;
126
144
  this.writeFormValue(submitted, String(this.pressed));
127
145
  }
@@ -142,7 +160,7 @@ var SonicButton = class extends SonicFormElement {
142
160
  this.#holding = holding;
143
161
  button.addEventListener("pointerdown", (event) => {
144
162
  if (event.button !== 0 || !this.#isMomentary()) return;
145
- button.setPointerCapture(event.pointerId);
163
+ capturePointer(button, event.pointerId);
146
164
  holding.hold(event.pointerId);
147
165
  }, { signal });
148
166
  button.addEventListener("keydown", (event) => {
@@ -150,6 +168,9 @@ var SonicButton = class extends SonicFormElement {
150
168
  holding.hold(event.key);
151
169
  }, { signal });
152
170
  }
171
+ #forwardPopup(button) {
172
+ for (const [name, aria] of Object.entries(popupAttributes)) writeAttribute(button, aria, this.getAttribute(name) ?? void 0);
173
+ }
153
174
  #isMomentary() {
154
175
  return this.momentary && !this.latching;
155
176
  }
@@ -160,6 +181,13 @@ var SonicButton = class extends SonicFormElement {
160
181
  this.pressed = isPressed;
161
182
  this.dispatchEvent(new Event("change", { bubbles: true }));
162
183
  }
184
+ #showLegend() {
185
+ const legend = this.legend;
186
+ for (const part of this.#button.querySelectorAll(":scope > .sonic-button-cap > [data-sonic-when]")) {
187
+ const when = part.dataset.sonicWhen ?? "";
188
+ part.toggleAttribute("data-sonic-hidden", !pressWhens.has(when) && when !== legend);
189
+ }
190
+ }
163
191
  };
164
192
 
165
193
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"button.js","names":[],"sources":["../../../src/elements/button.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-button': SonicButton;\n\t}\n}\n\nconst renderButton = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-button\" type=\"button\"><span class=\"sonic-button-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nexport class SonicButton extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t'busy',\n\t\t'momentary',\n\t\t'pressed',\n\t\t'soft-disabled',\n\t\t'latching',\n\t\t'value',\n\t];\n\n\tget armed(): boolean {\n\t\treturn this.hasAttribute('armed');\n\t}\n\n\tset armed(isArmed: boolean) {\n\t\tthis.reflect('armed', isArmed);\n\t}\n\n\tget busy(): boolean {\n\t\treturn this.hasAttribute('busy');\n\t}\n\n\tset busy(isBusy: boolean) {\n\t\tthis.reflect('busy', isBusy);\n\t}\n\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget latching(): boolean {\n\t\treturn this.hasAttribute('latching');\n\t}\n\n\tset latching(isLatching: boolean) {\n\t\tthis.reflect('latching', isLatching);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget softDisabled(): boolean {\n\t\treturn this.hasAttribute('soft-disabled');\n\t}\n\n\tset softDisabled(isSoftDisabled: boolean) {\n\t\tthis.reflect('soft-disabled', isSoftDisabled);\n\t}\n\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\treadonly #button = renderButton();\n\n\t#holding: Hold | undefined;\n\n\t#isDirty = false;\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (this.isDisabled() || this.#isSoftDisabled()) this.#holding?.release();\n\t\tthis.render();\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\tprotected override activate(): void {\n\t\tthis.#button.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst button = this.#button;\n\t\tconst cap = requireChild(button, '.sonic-button-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties(\n\t\t\t'armed',\n\t\t\t'busy',\n\t\t\t'defaultPressed',\n\t\t\t'momentary',\n\t\t\t'softDisabled',\n\t\t\t'latching',\n\t\t\t'pressed',\n\t\t\t'value',\n\t\t);\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcontrol: button,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tplaceChildren(cap, copies);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(button, 'button.css');\n\n\t\tbutton.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A native disabled button fires no click at all\n\t\t\t\tif (this.#isSoftDisabled()) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.latching) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(button, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#button;\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst button = this.#button;\n\n\t\twriteAttribute(button, 'disabled', this.isDisabled() ? '' : undefined);\n\t\twriteAttribute(button, 'aria-disabled', this.#isSoftDisabled() ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-busy', this.busy ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-pressed', this.latching ? String(this.pressed) : undefined);\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(button, true);\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.latching && this.pressed ? this.value : null;\n\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.latching) this.pressed = state === 'true';\n\t}\n\n\t#bindMomentary(button: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst holding = bindHold(\n\t\t\tbutton,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled() && !this.#isSoftDisabled(),\n\t\t\t\tleave: 'blur',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.#press(true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.#press(false);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\n\t\tthis.#holding = holding;\n\t\tbutton.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tbutton.setPointerCapture(event.pointerId);\n\t\t\t\tholding.hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tbutton.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tholding.hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.latching;\n\t}\n\n\t#isSoftDisabled(): boolean {\n\t\treturn this.softDisabled && !this.isDisabled();\n\t}\n\n\t#press(isPressed: boolean): void {\n\t\tthis.pressed = isPressed;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n}\n"],"mappings":";;;;;;;;AAeA,MAAM,eAAe,SACT;;IAGX,iBACD;AAEA,MAAM,2BAAW,IAAI,IAAI,CAAC,KAAK,OAAO,CAAC;AAEvC,IAAa,cAAb,cAAiC,iBAAiB;CACjD,OAAyB,qBAAqB;EAC7C,GAAG,iBAAiB;EACpB;EACA;EACA;EACA;EACA;EACA;CACD;CAEA,IAAI,QAAiB;EACpB,OAAO,KAAK,aAAa,OAAO;CACjC;CAEA,IAAI,MAAM,SAAkB;EAC3B,KAAK,QAAQ,SAAS,OAAO;CAC9B;CAEA,IAAI,OAAgB;EACnB,OAAO,KAAK,aAAa,MAAM;CAChC;CAEA,IAAI,KAAK,QAAiB;EACzB,KAAK,QAAQ,QAAQ,MAAM;CAC5B;CAEA,IAAI,iBAA0B;EAC7B,OAAO,KAAK,aAAa,SAAS;CACnC;CAEA,IAAI,eAAe,WAAoB;EACtC,KAAK,QAAQ,WAAW,SAAS;CAClC;CAEA,IAAI,WAAoB;EACvB,OAAO,KAAK,aAAa,UAAU;CACpC;CAEA,IAAI,SAAS,YAAqB;EACjC,KAAK,QAAQ,YAAY,UAAU;CACpC;CAEA,IAAI,YAAqB;EACxB,OAAO,KAAK,aAAa,WAAW;CACrC;CAEA,IAAI,UAAU,aAAsB;EACnC,KAAK,QAAQ,aAAa,WAAW;CACtC;CAEA,IAAI,UAAmB;EACtB,OAAO,KAAK,WAAW,KAAK,WAAW,KAAK;CAC7C;CAEA,IAAI,QAAQ,WAAgC;EAC3C,KAAK,WAAW;EAChB,KAAK,WAAW,cAAc;EAC9B,KAAK,OAAO;CACb;CAEA,IAAI,eAAwB;EAC3B,OAAO,KAAK,aAAa,eAAe;CACzC;CAEA,IAAI,aAAa,gBAAyB;EACzC,KAAK,QAAQ,iBAAiB,cAAc;CAC7C;CAEA,IAAI,QAAgB;EACnB,OAAO,KAAK,aAAa,OAAO,KAAK;CACtC;CAEA,IAAI,MAAM,OAA2B;EACpC,KAAK,QAAQ,SAAS,KAAK;CAC5B;CAEA,AAAS,UAAU,aAAa;CAEhC;CAEA,WAAW;CAEX,WAAW;CAEX,yBAAyB,MAAoB;EAC5C,IAAI,SAAS,WAAW,KAAK,WAAW;EACxC,IAAI,KAAK,WAAW,KAAK,KAAK,gBAAgB,GAAG,KAAK,UAAU,QAAQ;EACxE,KAAK,OAAO;CACb;CAEA,AAAS,oBAA0B;EAClC,KAAK,yBAAyB,SAAS;CACxC;CAEA,AAAmB,WAAiB;EACnC,KAAK,QAAQ,MAAM;CACpB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,SAAS,KAAK;EACpB,MAAM,MAAM,aAAa,QAAQ,qBAAqB,eAAe;EAErE,KAAK,kBACJ,SACA,QACA,kBACA,aACA,gBACA,YACA,WACA,OACD;EACA,eACC,MACA;GACC,SAAS;GACT,MAAM;GACN,QAAQ,WAAW;IAClB,cAAc,KAAK,MAAM;GAC1B;EACD,GACA,MACD;EACA,KAAK,OAAO;EACZ,KAAK,YAAY,QAAQ,YAAY;EAErC,OAAO,iBACN,UACC,UAAU;GAEV,IAAI,KAAK,gBAAgB,GAAG;IAC3B,MAAM,eAAe;IACrB,MAAM,yBAAyB;IAC/B;GACD;GACA,IAAI,CAAC,KAAK,UAAU;GAEpB,KAAK,UAAU,CAAC,KAAK;GACrB,KAAK,cAAc,IAAI,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC;EAC1D,GACA,EAAE,OAAO,CACV;EACA,KAAK,eAAe,QAAQ,MAAM;CACnC;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAU,SAAe;EACxB,IAAI,CAAC,KAAK,QAAQ,GAAG;EAErB,MAAM,SAAS,KAAK;EAEpB,eAAe,QAAQ,YAAY,KAAK,WAAW,IAAI,KAAK,MAAS;EACrE,eAAe,QAAQ,iBAAiB,KAAK,gBAAgB,IAAI,SAAS,MAAS;EACnF,eAAe,QAAQ,aAAa,KAAK,OAAO,SAAS,MAAS;EAClE,eAAe,QAAQ,gBAAgB,KAAK,WAAW,OAAO,KAAK,OAAO,IAAI,MAAS;EACvF,KAAK,YAAY,WAAW,KAAK,OAAO;EACxC,KAAK,cAAc,QAAQ,IAAI;EAG/B,MAAM,YAAY,KAAK,YAAY,KAAK,UAAU,KAAK,QAAQ;EAE/D,KAAK,eAAe,WAAW,OAAO,KAAK,OAAO,CAAC;CACpD;CAEA,AAAU,aAAa,OAAqB;EAC3C,IAAI,KAAK,UAAU,KAAK,UAAU,UAAU;CAC7C;CAEA,eAAe,QAA2B,QAA2B;EACpE,MAAM,UAAU,SACf,QACA;GACC,eAAe,CAAC,KAAK,WAAW,KAAK,CAAC,KAAK,gBAAgB;GAC3D,OAAO;GACP,cAAc;IACb,KAAK,OAAO,IAAI;GACjB;GACA,iBAAiB;IAChB,KAAK,OAAO,KAAK;GAClB;EACD,GACA,MACD;EAEA,KAAK,WAAW;EAChB,OAAO,iBACN,gBACC,UAAU;GACV,IAAI,MAAM,WAAW,KAAK,CAAC,KAAK,aAAa,GAAG;GAEhD,OAAO,kBAAkB,MAAM,SAAS;GACxC,QAAQ,KAAK,MAAM,SAAS;EAC7B,GACA,EAAE,OAAO,CACV;EACA,OAAO,iBACN,YACC,UAAU;GACV,IAAI,MAAM,UAAU,CAAC,SAAS,IAAI,MAAM,GAAG,KAAK,CAAC,KAAK,aAAa,GAAG;GAEtE,QAAQ,KAAK,MAAM,GAAG;EACvB,GACA,EAAE,OAAO,CACV;CACD;CAEA,eAAwB;EACvB,OAAO,KAAK,aAAa,CAAC,KAAK;CAChC;CAEA,kBAA2B;EAC1B,OAAO,KAAK,gBAAgB,CAAC,KAAK,WAAW;CAC9C;CAEA,OAAO,WAA0B;EAChC,KAAK,UAAU;EACf,KAAK,cAAc,IAAI,MAAM,UAAU,EAAE,SAAS,KAAK,CAAC,CAAC;CAC1D;AACD"}
1
+ {"version":3,"file":"button.js","names":[],"sources":["../../../src/elements/button.ts"],"sourcesContent":["import type { Hold } from '#lib/hold.ts';\n\nimport { SonicFormElement } from '#elements/form-element.ts';\nimport { copyNode } from '#lib/copy-node.ts';\nimport { bindHold } from '#lib/hold.ts';\nimport { mirrorChildren } from '#lib/mirror-children.ts';\nimport { capturePointer } from '#lib/pointer-drag.ts';\nimport { placeChildren, requireChild, template } from '#lib/render.ts';\nimport { writeAttribute } from '#lib/write-attribute.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-button': SonicButton;\n\t}\n}\n\nconst renderButton = template(\n\t/* HTML */ `\n\t\t<button class=\"sonic-button\" type=\"button\"><span class=\"sonic-button-cap\"></span></button>\n\t`,\n\tHTMLButtonElement,\n);\n\nconst holdKeys = new Set([' ', 'Enter']);\n\nconst popupAttributes = {\n\tcontrols: 'aria-controls',\n\texpanded: 'aria-expanded',\n\tpopup: 'aria-haspopup',\n} as const;\n\nconst pressWhens = new Set(['pressed', 'released']);\n\nexport class SonicButton extends SonicFormElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicFormElement.observedAttributes,\n\t\t...Object.keys(popupAttributes),\n\t\t'busy',\n\t\t'legend',\n\t\t'momentary',\n\t\t'pressed',\n\t\t'soft-disabled',\n\t\t'latching',\n\t\t'value',\n\t];\n\n\tget armed(): boolean {\n\t\treturn this.hasAttribute('armed');\n\t}\n\n\tset armed(isArmed: boolean) {\n\t\tthis.reflect('armed', isArmed);\n\t}\n\n\tget busy(): boolean {\n\t\treturn this.hasAttribute('busy');\n\t}\n\n\tset busy(isBusy: boolean) {\n\t\tthis.reflect('busy', isBusy);\n\t}\n\n\tget defaultPressed(): boolean {\n\t\treturn this.hasAttribute('pressed');\n\t}\n\n\tset defaultPressed(isPressed: boolean) {\n\t\tthis.reflect('pressed', isPressed);\n\t}\n\n\tget latching(): boolean {\n\t\treturn this.hasAttribute('latching');\n\t}\n\n\tset latching(isLatching: boolean) {\n\t\tthis.reflect('latching', isLatching);\n\t}\n\n\tget legend(): string | undefined {\n\t\treturn this.getAttribute('legend') ?? undefined;\n\t}\n\n\tset legend(name: string | undefined) {\n\t\tthis.reflect('legend', name);\n\t}\n\n\tget momentary(): boolean {\n\t\treturn this.hasAttribute('momentary');\n\t}\n\n\tset momentary(isMomentary: boolean) {\n\t\tthis.reflect('momentary', isMomentary);\n\t}\n\n\tget pressed(): boolean {\n\t\treturn this.#isDirty ? this.#pressed : this.defaultPressed;\n\t}\n\n\tset pressed(isPressed: boolean | undefined) {\n\t\tthis.#isDirty = true;\n\t\tthis.#pressed = isPressed === true;\n\t\tthis.render();\n\t}\n\n\tget softDisabled(): boolean {\n\t\treturn this.hasAttribute('soft-disabled');\n\t}\n\n\tset softDisabled(isSoftDisabled: boolean) {\n\t\tthis.reflect('soft-disabled', isSoftDisabled);\n\t}\n\n\tget value(): string {\n\t\treturn this.getAttribute('value') ?? 'on';\n\t}\n\n\tset value(value: string | undefined) {\n\t\tthis.reflect('value', value);\n\t}\n\n\treadonly #button = renderButton();\n\n\t#holding: Hold | undefined;\n\n\t#isDirty = false;\n\n\t#pressed = false;\n\n\tattributeChangedCallback(name: string): void {\n\t\tif (name === 'pressed') this.#isDirty = false;\n\t\tif (this.isDisabled() || this.#isSoftDisabled()) this.#holding?.release();\n\t\tthis.render();\n\t}\n\n\toverride formResetCallback(): void {\n\t\tthis.attributeChangedCallback('pressed');\n\t}\n\n\tprotected override activate(): void {\n\t\tthis.#button.click();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst button = this.#button;\n\t\tconst cap = requireChild(button, '.sonic-button-cap', HTMLSpanElement);\n\n\t\tthis.upgradeProperties(\n\t\t\t'armed',\n\t\t\t'busy',\n\t\t\t'defaultPressed',\n\t\t\t'legend',\n\t\t\t'momentary',\n\t\t\t'softDisabled',\n\t\t\t'latching',\n\t\t\t'pressed',\n\t\t\t'value',\n\t\t);\n\t\tmirrorChildren(\n\t\t\tthis,\n\t\t\t{\n\t\t\t\tcontrol: button,\n\t\t\t\tcopy: copyNode,\n\t\t\t\tplace: (copies) => {\n\t\t\t\t\tplaceChildren(cap, copies);\n\t\t\t\t\tthis.#showLegend();\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t\tthis.render();\n\t\tthis.checkStyles(button, 'button.css');\n\n\t\tbutton.addEventListener(\n\t\t\t'click',\n\t\t\t(event) => {\n\t\t\t\t// A native disabled button fires no click at all\n\t\t\t\tif (this.#isSoftDisabled()) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tevent.stopImmediatePropagation();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!this.latching) return;\n\n\t\t\t\tthis.pressed = !this.pressed;\n\t\t\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tthis.#bindMomentary(button, signal);\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#button;\n\t}\n\n\tprotected render(): void {\n\t\tif (!this.isBound()) return;\n\n\t\tconst button = this.#button;\n\n\t\twriteAttribute(button, 'disabled', this.isDisabled() ? '' : undefined);\n\t\twriteAttribute(button, 'aria-disabled', this.#isSoftDisabled() ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-busy', this.busy ? 'true' : undefined);\n\t\twriteAttribute(button, 'aria-pressed', this.latching ? String(this.pressed) : undefined);\n\t\tthis.toggleState('pressed', this.pressed);\n\t\tthis.forwardNaming(button, true);\n\t\tthis.#forwardPopup(button);\n\t\tthis.#showLegend();\n\n\t\t// eslint-disable-next-line unicorn/no-null -- `null` submits nothing\n\t\tconst submitted = this.latching && this.pressed ? this.value : null;\n\n\t\tthis.writeFormValue(submitted, String(this.pressed));\n\t}\n\n\tprotected restoreState(state: string): void {\n\t\tif (this.latching) this.pressed = state === 'true';\n\t}\n\n\t#bindMomentary(button: HTMLButtonElement, signal: AbortSignal): void {\n\t\tconst holding = bindHold(\n\t\t\tbutton,\n\t\t\t{\n\t\t\t\tcanHold: () => !this.isDisabled() && !this.#isSoftDisabled(),\n\t\t\t\tleave: 'blur',\n\t\t\t\tonHold: () => {\n\t\t\t\t\tthis.#press(true);\n\t\t\t\t},\n\t\t\t\tonRelease: () => {\n\t\t\t\t\tthis.#press(false);\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\n\t\tthis.#holding = holding;\n\t\tbutton.addEventListener(\n\t\t\t'pointerdown',\n\t\t\t(event) => {\n\t\t\t\tif (event.button !== 0 || !this.#isMomentary()) return;\n\n\t\t\t\tcapturePointer(button, event.pointerId);\n\t\t\t\tholding.hold(event.pointerId);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tbutton.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;\n\n\t\t\t\tholding.hold(event.key);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\n\t#forwardPopup(button: HTMLButtonElement): void {\n\t\tfor (const [name, aria] of Object.entries(popupAttributes)) {\n\t\t\twriteAttribute(button, aria, this.getAttribute(name) ?? undefined);\n\t\t}\n\t}\n\n\t#isMomentary(): boolean {\n\t\treturn this.momentary && !this.latching;\n\t}\n\n\t#isSoftDisabled(): boolean {\n\t\treturn this.softDisabled && !this.isDisabled();\n\t}\n\n\t#press(isPressed: boolean): void {\n\t\tthis.pressed = isPressed;\n\t\tthis.dispatchEvent(new Event('change', { bubbles: true }));\n\t}\n\n\t#showLegend(): void {\n\t\tconst legend = this.legend;\n\n\t\tfor (const part of this.#button.querySelectorAll<HTMLElement | SVGElement>(\n\t\t\t':scope > .sonic-button-cap > [data-sonic-when]',\n\t\t)) {\n\t\t\tconst when = part.dataset.sonicWhen ?? '';\n\n\t\t\tpart.toggleAttribute('data-sonic-hidden', !pressWhens.has(when) && when !== legend);\n\t\t}\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { SonicElement } from "./sonic-element.js";
2
+ import { Surface, SurfaceFrame } from "../lib/canvas-surface.js";
3
+ //#region src/elements/display-element.d.ts
4
+ export declare abstract class SonicDisplayElement<Colour extends string, Numeric extends string = never> extends SonicElement {
5
+ #private;
6
+ static readonly observedAttributes: string[];
7
+ get analyser(): AnalyserNode | undefined;
8
+ set analyser(node: AnalyserNode | null | undefined);
9
+ get max(): number;
10
+ set max(decibels: number | undefined);
11
+ get min(): number;
12
+ set min(decibels: number | undefined);
13
+ protected abstract readonly canvas: HTMLCanvasElement;
14
+ protected abstract readonly colours: Record<Colour, `--_sonic-${string}`>;
15
+ protected abstract readonly control: HTMLElement;
16
+ protected abstract readonly numbers: Record<Numeric, `--_sonic-${string}`>;
17
+ protected abstract readonly sheet: string;
18
+ attributeChangedCallback(): void;
19
+ connectedCallback(): void;
20
+ push(frame: Float32Array): void;
21
+ repaint(): void;
22
+ protected connect(signal: AbortSignal): void;
23
+ protected abstract paint(context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, never, Numeric>): void;
24
+ protected abstract pull(analyser: AnalyserNode, buffer: Float32Array<ArrayBuffer>): void;
25
+ protected abstract receive(frame: Float32Array): void;
26
+ protected surface(): Surface | undefined;
27
+ }
28
+ //#endregion
29
+ //# sourceMappingURL=display-element.d.ts.map
@@ -0,0 +1,78 @@
1
+ import { SonicElement } from "./sonic-element.js";
2
+ import { bindSurface } from "../lib/canvas-surface.js";
3
+
4
+ //#region src/elements/display-element.ts
5
+ var SonicDisplayElement = class extends SonicElement {
6
+ static observedAttributes = [
7
+ ...SonicElement.observedAttributes,
8
+ "max",
9
+ "min"
10
+ ];
11
+ get analyser() {
12
+ return this.#analyser;
13
+ }
14
+ set analyser(node) {
15
+ this.#analyser = node ?? void 0;
16
+ this.#surface?.requestFrame();
17
+ }
18
+ get max() {
19
+ return this.numberAttribute("max", 0);
20
+ }
21
+ set max(decibels) {
22
+ this.reflect("max", decibels);
23
+ }
24
+ get min() {
25
+ return this.numberAttribute("min", -90);
26
+ }
27
+ set min(decibels) {
28
+ this.reflect("min", decibels);
29
+ }
30
+ #analyser;
31
+ #buffer = /* @__PURE__ */ new Float32Array(0);
32
+ #surface;
33
+ attributeChangedCallback() {
34
+ this.#surface?.invalidate();
35
+ }
36
+ connectedCallback() {
37
+ this.upgradeProperties("analyser", "max", "min");
38
+ super.connectedCallback();
39
+ }
40
+ push(frame) {
41
+ this.receive(frame);
42
+ this.#surface?.requestFrame();
43
+ }
44
+ repaint() {
45
+ this.#surface?.rebuild();
46
+ }
47
+ connect(signal) {
48
+ const control = this.control;
49
+ this.keepControl(control, signal);
50
+ this.#surface = bindSurface({
51
+ canvas: this.canvas,
52
+ colours: this.colours,
53
+ numbers: this.numbers,
54
+ paint: (context, frame) => {
55
+ this.#paint(context, frame);
56
+ },
57
+ signal
58
+ });
59
+ this.checkStyles(control, this.sheet);
60
+ }
61
+ surface() {
62
+ return this.#surface;
63
+ }
64
+ #paint(context, frame) {
65
+ const analyser = this.isDisabled() ? void 0 : this.#analyser;
66
+ if (analyser) {
67
+ if (this.#buffer.length !== analyser.frequencyBinCount) this.#buffer = new Float32Array(analyser.frequencyBinCount);
68
+ this.pull(analyser, this.#buffer);
69
+ this.receive(this.#buffer);
70
+ }
71
+ this.paint(context, frame);
72
+ if (analyser) this.#surface?.requestFrame();
73
+ }
74
+ };
75
+
76
+ //#endregion
77
+ export { SonicDisplayElement };
78
+ //# sourceMappingURL=display-element.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"display-element.js","names":[],"sources":["../../../src/elements/display-element.ts"],"sourcesContent":["import type { Surface, SurfaceFrame } from '#lib/canvas-surface.ts';\n\nimport { SonicElement } from '#elements/sonic-element.ts';\nimport { bindSurface } from '#lib/canvas-surface.ts';\n\nexport abstract class SonicDisplayElement<\n\tColour extends string,\n\tNumeric extends string = never,\n> extends SonicElement {\n\tstatic override readonly observedAttributes = [...SonicElement.observedAttributes, 'max', 'min'];\n\n\tget analyser(): AnalyserNode | undefined {\n\t\treturn this.#analyser;\n\t}\n\n\tset analyser(node: AnalyserNode | null | undefined) {\n\t\tthis.#analyser = node ?? undefined;\n\t\tthis.#surface?.requestFrame();\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- one accessor pair per reflected attribute\n\tget max(): number {\n\t\treturn this.numberAttribute('max', 0);\n\t}\n\n\tset max(decibels: number | undefined) {\n\t\tthis.reflect('max', decibels);\n\t}\n\n\tget min(): number {\n\t\treturn this.numberAttribute('min', -90);\n\t}\n\n\tset min(decibels: number | undefined) {\n\t\tthis.reflect('min', decibels);\n\t}\n\n\tprotected abstract readonly canvas: HTMLCanvasElement;\n\n\tprotected abstract readonly colours: Record<Colour, `--_sonic-${string}`>;\n\n\tprotected abstract readonly control: HTMLElement;\n\n\tprotected abstract readonly numbers: Record<Numeric, `--_sonic-${string}`>;\n\n\tprotected abstract readonly sheet: string;\n\n\t#analyser: AnalyserNode | undefined;\n\n\t#buffer = new Float32Array(0);\n\n\t#surface: Surface | undefined;\n\n\tattributeChangedCallback(): void {\n\t\tthis.#surface?.invalidate();\n\t}\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('analyser', 'max', 'min');\n\t\tsuper.connectedCallback();\n\t}\n\n\tpush(frame: Float32Array): void {\n\t\tthis.receive(frame);\n\t\tthis.#surface?.requestFrame();\n\t}\n\n\t// fallow-ignore-next-line code-duplication -- the wave controls keep their own copy of the surface binding\n\trepaint(): void {\n\t\tthis.#surface?.rebuild();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst control = this.control;\n\n\t\tthis.keepControl(control, signal);\n\t\tthis.#surface = bindSurface({\n\t\t\tcanvas: this.canvas,\n\t\t\tcolours: this.colours,\n\t\t\tnumbers: this.numbers,\n\t\t\tpaint: (context, frame) => {\n\t\t\t\tthis.#paint(context, frame);\n\t\t\t},\n\t\t\tsignal,\n\t\t});\n\t\tthis.checkStyles(control, this.sheet);\n\t}\n\n\tprotected abstract paint(\n\t\tcontext: CanvasRenderingContext2D,\n\t\tframe: SurfaceFrame<Colour, never, Numeric>,\n\t): void;\n\n\tprotected abstract pull(analyser: AnalyserNode, buffer: Float32Array<ArrayBuffer>): void;\n\n\tprotected abstract receive(frame: Float32Array): void;\n\n\tprotected surface(): Surface | undefined {\n\t\treturn this.#surface;\n\t}\n\n\t// The node never signals that sound came back, so every paint pulls\n\t#paint(context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, never, Numeric>): void {\n\t\tconst analyser = this.isDisabled() ? undefined : this.#analyser;\n\n\t\tif (analyser) {\n\t\t\tif (this.#buffer.length !== analyser.frequencyBinCount) {\n\t\t\t\tthis.#buffer = new Float32Array(analyser.frequencyBinCount);\n\t\t\t}\n\t\t\tthis.pull(analyser, this.#buffer);\n\t\t\tthis.receive(this.#buffer);\n\t\t}\n\t\tthis.paint(context, frame);\n\t\tif (analyser) this.#surface?.requestFrame();\n\t}\n}\n"],"mappings":";;;;AAKA,IAAsB,sBAAtB,cAGU,aAAa;CACtB,OAAyB,qBAAqB;EAAC,GAAG,aAAa;EAAoB;EAAO;CAAK;CAE/F,IAAI,WAAqC;EACxC,OAAO,KAAK;CACb;CAEA,IAAI,SAAS,MAAuC;EACnD,KAAK,YAAY,QAAQ;EACzB,KAAK,UAAU,aAAa;CAC7B;CAGA,IAAI,MAAc;EACjB,OAAO,KAAK,gBAAgB,OAAO,CAAC;CACrC;CAEA,IAAI,IAAI,UAA8B;EACrC,KAAK,QAAQ,OAAO,QAAQ;CAC7B;CAEA,IAAI,MAAc;EACjB,OAAO,KAAK,gBAAgB,OAAO,GAAG;CACvC;CAEA,IAAI,IAAI,UAA8B;EACrC,KAAK,QAAQ,OAAO,QAAQ;CAC7B;CAYA;CAEA,0BAAU,IAAI,aAAa,CAAC;CAE5B;CAEA,2BAAiC;EAChC,KAAK,UAAU,WAAW;CAC3B;CAEA,AAAS,oBAA0B;EAClC,KAAK,kBAAkB,YAAY,OAAO,KAAK;EAC/C,MAAM,kBAAkB;CACzB;CAEA,KAAK,OAA2B;EAC/B,KAAK,QAAQ,KAAK;EAClB,KAAK,UAAU,aAAa;CAC7B;CAGA,UAAgB;EACf,KAAK,UAAU,QAAQ;CACxB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,UAAU,KAAK;EAErB,KAAK,YAAY,SAAS,MAAM;EAChC,KAAK,WAAW,YAAY;GAC3B,QAAQ,KAAK;GACb,SAAS,KAAK;GACd,SAAS,KAAK;GACd,QAAQ,SAAS,UAAU;IAC1B,KAAK,OAAO,SAAS,KAAK;GAC3B;GACA;EACD,CAAC;EACD,KAAK,YAAY,SAAS,KAAK,KAAK;CACrC;CAWA,AAAU,UAA+B;EACxC,OAAO,KAAK;CACb;CAGA,OAAO,SAAmC,OAAmD;EAC5F,MAAM,WAAW,KAAK,WAAW,IAAI,SAAY,KAAK;EAEtD,IAAI,UAAU;GACb,IAAI,KAAK,QAAQ,WAAW,SAAS,mBACpC,KAAK,UAAU,IAAI,aAAa,SAAS,iBAAiB;GAE3D,KAAK,KAAK,UAAU,KAAK,OAAO;GAChC,KAAK,QAAQ,KAAK,OAAO;EAC1B;EACA,KAAK,MAAM,SAAS,KAAK;EACzB,IAAI,UAAU,KAAK,UAAU,aAAa;CAC3C;AACD"}