@xsynaptic/sonic-ui 0.24.1 → 0.26.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 (137) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/README.md +6 -4
  3. package/dist/default/define/region.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/button.js +3 -0
  6. package/dist/default/elements/dial.js +3 -0
  7. package/dist/default/elements/envelope.js +14 -11
  8. package/dist/default/elements/form-element.js +10 -0
  9. package/dist/default/elements/meter.js +3 -0
  10. package/dist/default/elements/number.js +3 -0
  11. package/dist/default/elements/radio-group.js +4 -1
  12. package/dist/default/elements/readout.js +1 -1
  13. package/dist/default/elements/region-layer.js +253 -0
  14. package/dist/default/elements/region.js +161 -0
  15. package/dist/default/elements/slider.js +3 -0
  16. package/dist/default/elements/sonic-element.js +7 -9
  17. package/dist/default/elements/split.js +2 -2
  18. package/dist/default/elements/toggle.js +4 -4
  19. package/dist/default/elements/value-element.js +21 -3
  20. package/dist/default/elements/value-gestures.js +7 -5
  21. package/dist/default/elements/wave-element.js +15 -3
  22. package/dist/default/elements/waveform.js +38 -2
  23. package/dist/default/elements/wavestrip.js +90 -17
  24. package/dist/default/elements/xy.js +6 -11
  25. package/dist/default/index.js +2 -1
  26. package/dist/default/lib/canvas-surface.js +22 -1
  27. package/dist/default/lib/clock.js +1 -0
  28. package/dist/default/lib/drag-step.js +1 -1
  29. package/dist/default/lib/key-press.js +2 -2
  30. package/dist/default/lib/marker-band.js +40 -29
  31. package/dist/default/lib/region-drag.js +48 -0
  32. package/dist/dev/define/region.d.ts +2 -0
  33. package/dist/dev/define/region.js +8 -0
  34. package/dist/dev/define/region.js.map +1 -0
  35. package/dist/dev/define.d.ts +3 -1
  36. package/dist/dev/define.js +1 -0
  37. package/dist/dev/elements/button.d.ts +1 -0
  38. package/dist/dev/elements/button.js +3 -0
  39. package/dist/dev/elements/button.js.map +1 -1
  40. package/dist/dev/elements/dial.d.ts +1 -0
  41. package/dist/dev/elements/dial.js +3 -0
  42. package/dist/dev/elements/dial.js.map +1 -1
  43. package/dist/dev/elements/envelope.d.ts +1 -0
  44. package/dist/dev/elements/envelope.js +14 -11
  45. package/dist/dev/elements/envelope.js.map +1 -1
  46. package/dist/dev/elements/form-element.d.ts +1 -0
  47. package/dist/dev/elements/form-element.js +10 -0
  48. package/dist/dev/elements/form-element.js.map +1 -1
  49. package/dist/dev/elements/meter.d.ts +1 -0
  50. package/dist/dev/elements/meter.js +3 -0
  51. package/dist/dev/elements/meter.js.map +1 -1
  52. package/dist/dev/elements/number.d.ts +1 -0
  53. package/dist/dev/elements/number.js +3 -0
  54. package/dist/dev/elements/number.js.map +1 -1
  55. package/dist/dev/elements/radio-group.d.ts +1 -0
  56. package/dist/dev/elements/radio-group.js +4 -1
  57. package/dist/dev/elements/radio-group.js.map +1 -1
  58. package/dist/dev/elements/readout.js +1 -1
  59. package/dist/dev/elements/readout.js.map +1 -1
  60. package/dist/dev/elements/region-layer.js +254 -0
  61. package/dist/dev/elements/region-layer.js.map +1 -0
  62. package/dist/dev/elements/region.d.ts +30 -0
  63. package/dist/dev/elements/region.js +169 -0
  64. package/dist/dev/elements/region.js.map +1 -0
  65. package/dist/dev/elements/slider.d.ts +1 -0
  66. package/dist/dev/elements/slider.js +3 -0
  67. package/dist/dev/elements/slider.js.map +1 -1
  68. package/dist/dev/elements/sonic-element.d.ts +2 -0
  69. package/dist/dev/elements/sonic-element.js +8 -10
  70. package/dist/dev/elements/sonic-element.js.map +1 -1
  71. package/dist/dev/elements/split.js +2 -2
  72. package/dist/dev/elements/split.js.map +1 -1
  73. package/dist/dev/elements/toggle.js +4 -4
  74. package/dist/dev/elements/toggle.js.map +1 -1
  75. package/dist/dev/elements/value-element.d.ts +3 -1
  76. package/dist/dev/elements/value-element.js +21 -3
  77. package/dist/dev/elements/value-element.js.map +1 -1
  78. package/dist/dev/elements/value-gestures.js +7 -5
  79. package/dist/dev/elements/value-gestures.js.map +1 -1
  80. package/dist/dev/elements/wave-element.d.ts +8 -1
  81. package/dist/dev/elements/wave-element.js +15 -3
  82. package/dist/dev/elements/wave-element.js.map +1 -1
  83. package/dist/dev/elements/waveform.d.ts +7 -0
  84. package/dist/dev/elements/waveform.js +38 -2
  85. package/dist/dev/elements/waveform.js.map +1 -1
  86. package/dist/dev/elements/wavestrip.d.ts +17 -1
  87. package/dist/dev/elements/wavestrip.js +90 -17
  88. package/dist/dev/elements/wavestrip.js.map +1 -1
  89. package/dist/dev/elements/xy.d.ts +1 -0
  90. package/dist/dev/elements/xy.js +6 -11
  91. package/dist/dev/elements/xy.js.map +1 -1
  92. package/dist/dev/index.d.ts +3 -2
  93. package/dist/dev/index.js +2 -1
  94. package/dist/dev/lib/canvas-surface.js +22 -1
  95. package/dist/dev/lib/canvas-surface.js.map +1 -1
  96. package/dist/dev/lib/clock.js +1 -0
  97. package/dist/dev/lib/clock.js.map +1 -1
  98. package/dist/dev/lib/drag-step.js +1 -1
  99. package/dist/dev/lib/drag-step.js.map +1 -1
  100. package/dist/dev/lib/key-press.js +2 -2
  101. package/dist/dev/lib/key-press.js.map +1 -1
  102. package/dist/dev/lib/marker-band.d.ts +7 -0
  103. package/dist/dev/lib/marker-band.js +40 -29
  104. package/dist/dev/lib/marker-band.js.map +1 -1
  105. package/dist/dev/lib/region-drag.js +49 -0
  106. package/dist/dev/lib/region-drag.js.map +1 -0
  107. package/dist/styles/button.css +23 -13
  108. package/dist/styles/controls.css +1 -0
  109. package/dist/styles/dial.css +41 -24
  110. package/dist/styles/envelope.css +12 -5
  111. package/dist/styles/led.css +8 -8
  112. package/dist/styles/material/arc.css +2 -2
  113. package/dist/styles/material/cap.css +34 -13
  114. package/dist/styles/material/glass.css +1 -1
  115. package/dist/styles/material/groove.css +3 -3
  116. package/dist/styles/material/keycap.css +2 -2
  117. package/dist/styles/material/lens.css +1 -1
  118. package/dist/styles/material/pane.css +20 -6
  119. package/dist/styles/material/readout.css +14 -5
  120. package/dist/styles/material/{bracket.css → reticle.css} +15 -8
  121. package/dist/styles/material/well.css +2 -2
  122. package/dist/styles/material.css +1 -1
  123. package/dist/styles/meter.css +29 -16
  124. package/dist/styles/number.css +3 -2
  125. package/dist/styles/panel.css +3 -3
  126. package/dist/styles/region.css +94 -0
  127. package/dist/styles/ring.css +2 -1
  128. package/dist/styles/screen.css +7 -5
  129. package/dist/styles/segmented.css +5 -4
  130. package/dist/styles/slider.css +24 -15
  131. package/dist/styles/spectrum.css +9 -1
  132. package/dist/styles/switch.css +9 -2
  133. package/dist/styles/toggle.css +5 -4
  134. package/dist/styles/waveform.css +10 -3
  135. package/dist/styles/wavestrip.css +17 -8
  136. package/dist/styles/xy.css +24 -11
  137. package/package.json +3 -2
@@ -0,0 +1,161 @@
1
+ import { SonicFormElement } from "./form-element.js";
2
+
3
+ //#region src/elements/region.ts
4
+ const owners = /* @__PURE__ */ new WeakMap();
5
+ let link;
6
+ function linkRegion(element) {
7
+ return link(element);
8
+ }
9
+ function ownRegions(host, changed, signal) {
10
+ owners.set(host, changed);
11
+ signal.addEventListener("abort", () => {
12
+ if (owners.get(host) === changed) owners.delete(host);
13
+ }, { once: true });
14
+ }
15
+ var SonicRegion = class extends SonicFormElement {
16
+ static observedAttributes = [
17
+ ...SonicFormElement.observedAttributes,
18
+ "end",
19
+ "kind",
20
+ "name",
21
+ "start",
22
+ "tabindex"
23
+ ];
24
+ static {
25
+ link = (element) => ({
26
+ change: () => {
27
+ element.#report("change");
28
+ },
29
+ draw: (part) => {
30
+ element.#draw(part);
31
+ },
32
+ hold: (by) => {
33
+ element.#held = by;
34
+ element.toggleState("dragging", by === "pointer");
35
+ },
36
+ input: (span) => element.#input(span),
37
+ isDisabled: () => element.isDisabled(),
38
+ span: () => {
39
+ const { end, start } = element.#span;
40
+ return {
41
+ end: Math.max(start, end),
42
+ start: Math.min(start, end)
43
+ };
44
+ }
45
+ });
46
+ }
47
+ get dragging() {
48
+ return this.hasState("dragging");
49
+ }
50
+ get end() {
51
+ return this.#span.end;
52
+ }
53
+ set end(next) {
54
+ if (!this.#held) this.reflect("end", next);
55
+ }
56
+ /** Lowercase letters, digits and hyphens; colours the region through `--sonic-marker-<kind>` */
57
+ get kind() {
58
+ return this.getAttribute("kind") ?? void 0;
59
+ }
60
+ set kind(kind) {
61
+ this.reflect("kind", kind);
62
+ }
63
+ get start() {
64
+ return this.#span.start;
65
+ }
66
+ set start(next) {
67
+ if (!this.#held) this.reflect("start", next);
68
+ }
69
+ #formState = "";
70
+ #held;
71
+ #part;
72
+ #span = {
73
+ end: 0,
74
+ start: 0
75
+ };
76
+ attributeChangedCallback(name) {
77
+ const isSpan = name === "end" || name === "start";
78
+ if (isSpan && this.#held) return;
79
+ if (isSpan) this.#readSpan(name);
80
+ this.render();
81
+ }
82
+ connectedCallback() {
83
+ this.upgradeProperties("end", "kind", "start");
84
+ super.connectedCallback();
85
+ }
86
+ formResetCallback() {
87
+ if (this.#held) return;
88
+ this.#readSpan("start");
89
+ this.#readSpan("end");
90
+ this.render();
91
+ }
92
+ connect() {
93
+ this.render();
94
+ if (this.parentElement?.localName !== "sonic-wavestrip");
95
+ }
96
+ focusTarget() {
97
+ return this.#part;
98
+ }
99
+ render() {
100
+ if (!this.isBound() || !this.parentElement) return;
101
+ owners.get(this.parentElement)?.(this);
102
+ }
103
+ restoreState(state) {
104
+ const [start = NaN, end = NaN] = state.split(",").map(Number);
105
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return;
106
+ this.#span = {
107
+ end,
108
+ start
109
+ };
110
+ this.render();
111
+ }
112
+ stateTarget() {
113
+ return this.#part;
114
+ }
115
+ #draw(part) {
116
+ this.#part = part;
117
+ this.forwardNaming(part, true);
118
+ this.#writeForm();
119
+ }
120
+ #input(span) {
121
+ const hasMoved = span.start !== this.#span.start || span.end !== this.#span.end;
122
+ if (hasMoved) {
123
+ this.#span = span;
124
+ this.render();
125
+ this.#report("input");
126
+ }
127
+ return hasMoved;
128
+ }
129
+ #readSpan(name) {
130
+ const { end, start } = this.#span;
131
+ if (name === "end") {
132
+ this.#span = {
133
+ end: this.numberAttribute("end", start),
134
+ start
135
+ };
136
+ return;
137
+ }
138
+ const next = this.numberAttribute("start", 0);
139
+ this.#span = {
140
+ end: this.hasAttribute("end") ? end : next,
141
+ start: next
142
+ };
143
+ }
144
+ #report(type) {
145
+ this.dispatchEvent(new Event(type, {
146
+ bubbles: true,
147
+ composed: type === "input"
148
+ }));
149
+ }
150
+ #writeForm() {
151
+ const { end, start } = this.#span;
152
+ const state = `${String(start)},${String(end)}`;
153
+ const formState = `${this.name}=${state}`;
154
+ if (formState === this.#formState) return;
155
+ this.#formState = formState;
156
+ this.writeFormFields([["start", start], ["end", end]], state);
157
+ }
158
+ };
159
+
160
+ //#endregion
161
+ export { SonicRegion, linkRegion, ownRegions };
@@ -152,6 +152,9 @@ var SonicSlider = class extends SonicValueElement {
152
152
  springTarget() {
153
153
  return this.spring ? this.originValue() : void 0;
154
154
  }
155
+ stateTarget() {
156
+ return this.#slider;
157
+ }
155
158
  #axis(slider, cap) {
156
159
  const box = slider.getBoundingClientRect();
157
160
  const capBox = cap.getBoundingClientRect();
@@ -8,7 +8,6 @@ const namingAttributes = [
8
8
  "aria-label",
9
9
  "aria-labelledby"
10
10
  ];
11
- const canMatchStates = CSS.supports("selector(:state(a))");
12
11
  const canReferElements = "ariaLabelledByElements" in Element.prototype;
13
12
  function writeLabelledBy(target, labels) {
14
13
  const current = target.ariaLabelledByElements;
@@ -26,7 +25,6 @@ var SonicElement = class SonicElement extends HTMLElement {
26
25
  }
27
26
  internals = "attachInternals" in HTMLElement.prototype ? this.attachInternals() : void 0;
28
27
  #connection;
29
- #customStates = canMatchStates ? this.internals?.states : void 0;
30
28
  #states = /* @__PURE__ */ new Set();
31
29
  static #labelId(label) {
32
30
  if (label.id === "") {
@@ -118,14 +116,11 @@ var SonicElement = class SonicElement extends HTMLElement {
118
116
  }
119
117
  this.setAttribute(name, value === true ? "" : value.toString());
120
118
  }
119
+ stateTarget() {}
121
120
  toggleState(state, isOn) {
122
- if (isOn) {
123
- this.#states.add(state);
124
- this.#customStates?.add(state);
125
- return;
126
- }
127
- this.#states.delete(state);
128
- this.#customStates?.delete(state);
121
+ if (isOn) this.#states.add(state);
122
+ else this.#states.delete(state);
123
+ this.stateTarget()?.toggleAttribute(`data-sonic-${state}`, isOn);
129
124
  }
130
125
  upgradeProperties(...names) {
131
126
  for (const name of names) {
@@ -135,6 +130,9 @@ var SonicElement = class SonicElement extends HTMLElement {
135
130
  this[name] = value;
136
131
  }
137
132
  }
133
+ usesChild(_child) {
134
+ return false;
135
+ }
138
136
  #checkBox(control, checked, box) {}
139
137
  #checkHost(checked) {}
140
138
  #focused() {
@@ -8,7 +8,7 @@ const modeNames = /* @__PURE__ */ new Set([
8
8
  "equal",
9
9
  "proportional"
10
10
  ]);
11
- const gridSlop = 1e-9;
11
+ const gridTolerance = 1e-9;
12
12
  const lockAttributes = ["data-sonic-locked", "disabled"];
13
13
  function isMode(name) {
14
14
  return name !== null && modeNames.has(name);
@@ -19,7 +19,7 @@ function isLocked(member) {
19
19
  function gridLimits(mapping, [low, high]) {
20
20
  const bottom = mapping.snap(low);
21
21
  const top = mapping.snap(high);
22
- return [bottom < low - gridSlop ? mapping.keyTarget("ArrowRight", bottom) ?? bottom : bottom, top > high + gridSlop ? mapping.keyTarget("ArrowLeft", top) ?? top : top];
22
+ return [bottom < low - gridTolerance ? mapping.keyTarget("ArrowRight", bottom) ?? bottom : bottom, top > high + gridTolerance ? mapping.keyTarget("ArrowLeft", top) ?? top : top];
23
23
  }
24
24
  function modelOf(member, value, isFree) {
25
25
  const mapping = member.link.model.mapping();
@@ -19,7 +19,7 @@ const renderPosition = template(`
19
19
  </button>
20
20
  `, HTMLButtonElement);
21
21
  const renderBare = template(`<button class="sonic-toggle-position" role="switch" type="button"></button>`, HTMLButtonElement);
22
- const pressSlop = 2;
22
+ const pressTolerance = 2;
23
23
  var SonicToggle = class extends SonicPositionGroupElement {
24
24
  bare = renderBare();
25
25
  defaultOrientation = "horizontal";
@@ -72,11 +72,11 @@ var SonicToggle = class extends SonicPositionGroupElement {
72
72
  if (!drag.hasMoved && moved < drag.thresholdPx) return;
73
73
  drag.hasMoved = true;
74
74
  this.toggleState("dragging", true);
75
- if (moved >= pressSlop * drag.thresholdPx) drag.isPress = false;
75
+ if (moved >= pressTolerance * drag.thresholdPx) drag.isPress = false;
76
76
  if (this.#slideTo(positionAt(drag.travel, pointer), event.pointerId)) drag.isPress = false;
77
77
  },
78
78
  release: (drag) => {
79
- delete drag.marked.dataset.sonicPressed;
79
+ delete drag.marked.dataset.sonicActive;
80
80
  this.#draggedTo = void 0;
81
81
  this.toggleState("dragging", false);
82
82
  this.render();
@@ -94,7 +94,7 @@ var SonicToggle = class extends SonicPositionGroupElement {
94
94
  const travel = this.#travel(event, isOnCap);
95
95
  if (!travel || !isOnCap && this.isOptionDisabled(position)) return void 0;
96
96
  const marked = this.options()[position] ?? this.bare;
97
- marked.toggleAttribute("data-sonic-pressed", true);
97
+ marked.toggleAttribute("data-sonic-active", true);
98
98
  return {
99
99
  hasMoved: false,
100
100
  isHolding: this.#holdFrom(position, event.pointerId),
@@ -233,6 +233,7 @@ var SonicValueElement = class extends SonicFormElement {
233
233
  #model = createValueModel(() => this.#spec());
234
234
  #parseValue;
235
235
  #readout;
236
+ #said;
236
237
  attributeChangedCallback(name) {
237
238
  if (name === "value" && this.#isHeld()) {
238
239
  this.#writeHeld(this.numberAttribute("value", this.min));
@@ -286,16 +287,17 @@ var SonicValueElement = class extends SonicFormElement {
286
287
  toggleState: (state, isOn) => {
287
288
  this.toggleState(state, isOn);
288
289
  }
289
- }, control);
290
+ }, control, this.focusTarget());
290
291
  this.#gestures.bind(signal, grab);
291
292
  control.addEventListener("pointerup", (event) => {
292
293
  this.#hover?.lift(event);
293
294
  }, { signal });
294
295
  }
295
- bindHover(control, signal, { canShow = () => true, place, valueAt }) {
296
+ bindHover(control, signal, { canShow = () => true, changed, place, valueAt }) {
296
297
  this.#hover = bindHoverPreview(control, {
297
298
  canShow,
298
299
  changed: () => {
300
+ changed?.();
299
301
  this.dispatchEvent(new Event("sonic-hover", { bubbles: true }));
300
302
  },
301
303
  dismiss: () => {
@@ -367,6 +369,9 @@ var SonicValueElement = class extends SonicFormElement {
367
369
  played: from === void 0 ? this.#model.value : this.#heldAt ?? from
368
370
  };
369
371
  }
372
+ spokenText(value) {
373
+ return (this.#formatSpokenValue ?? this.#formatValue ?? this.notation.speak)?.(value);
374
+ }
370
375
  springTarget() {}
371
376
  tapTarget() {}
372
377
  travelPx(control, property) {
@@ -401,7 +406,7 @@ var SonicValueElement = class extends SonicFormElement {
401
406
  writeAttribute(control, "aria-valuemax", String(high));
402
407
  writeAttribute(control, "aria-valuenow", String(this.#spokenNow()));
403
408
  if (orientation) writeAttribute(control, "aria-orientation", orientation);
404
- writeAttribute(control, "aria-valuetext", (this.#formatSpokenValue ?? this.#formatValue ?? this.notation.speak)?.(this.#spokenNow()));
409
+ writeAttribute(control, "aria-valuetext", this.#spokenValueText());
405
410
  this.forwardNaming(control, true);
406
411
  this.#renderDisabled(control);
407
412
  this.renderReadout();
@@ -454,6 +459,19 @@ var SonicValueElement = class extends SonicFormElement {
454
459
  const step = this.spokenStep;
455
460
  return step !== void 0 && step > 0 ? trimFloat(Math.round(this.#model.value / step) * step) : this.#model.value;
456
461
  }
462
+ #spokenValueText() {
463
+ const format = this.#formatSpokenValue ?? this.#formatValue ?? this.notation.speak;
464
+ const value = this.#spokenNow();
465
+ const said = this.#said;
466
+ if (said && said.format === format && said.value === value) return said.text;
467
+ const text = format?.(value);
468
+ this.#said = {
469
+ format,
470
+ text,
471
+ value
472
+ };
473
+ return text;
474
+ }
457
475
  #textFor(value) {
458
476
  return (this.#formatValue ?? this.notation.format)(value);
459
477
  }
@@ -18,14 +18,16 @@ var ValueGestures = class {
18
18
  });
19
19
  #control;
20
20
  #entry;
21
+ #focus;
21
22
  #host;
22
23
  #isRevealed = false;
23
24
  #keyScrub;
24
25
  #pointerDrag;
25
26
  #pointerType;
26
- constructor(host, control) {
27
+ constructor(host, control, focus) {
27
28
  this.#host = host;
28
29
  this.#control = control;
30
+ this.#focus = focus;
29
31
  }
30
32
  abandon() {
31
33
  const drag = this.#dragging();
@@ -46,7 +48,7 @@ var ValueGestures = class {
46
48
  if (isMenuPress(event) || host.isDisabled() || entry.isOpen) return;
47
49
  host.toggleState("springing", false);
48
50
  if (event.pointerType === "touch") event.preventDefault();
49
- focusByPointer(control);
51
+ focusByPointer(this.#focus);
50
52
  this.#pointerType = event.pointerType;
51
53
  if (isResetPress(event)) {
52
54
  this.#reset();
@@ -135,14 +137,14 @@ var ValueGestures = class {
135
137
  }
136
138
  });
137
139
  this.#entry = entry;
138
- entry.bind(control, signal);
140
+ entry.bind(this.#focus, signal);
139
141
  return entry;
140
142
  }
141
143
  #bindKeys(entry, signal) {
142
144
  const control = this.#control;
143
145
  const host = this.#host;
144
146
  control.addEventListener("keydown", (event) => {
145
- if (host.isDisabled() || event.defaultPrevented || event.target !== control) return;
147
+ if (host.isDisabled() || event.defaultPrevented || event.target !== this.#focus) return;
146
148
  host.toggleState("springing", false);
147
149
  if (event.key === "Enter") {
148
150
  if (host.springTarget() !== void 0 || !host.hasEntry()) return;
@@ -164,7 +166,7 @@ var ValueGestures = class {
164
166
  if (isMeta && this.#dragging()) return;
165
167
  if (isMeta || host.model.mapping().keyTarget(event.key, host.model.value) !== void 0) this.#springBack();
166
168
  }, { signal });
167
- control.addEventListener("blur", () => {
169
+ this.#focus.addEventListener("blur", () => {
168
170
  this.#endKeyScrub();
169
171
  this.#concealKeyReveal();
170
172
  this.#springBack();
@@ -48,6 +48,18 @@ var SonicWaveElement = class extends SonicValueElement {
48
48
  this.upgradeProperties("fill", "markers");
49
49
  super.connectedCallback();
50
50
  }
51
+ /** Copies; none outside the canvas */
52
+ markersFromPoint(clientX, clientY) {
53
+ const box = this.canvas.getBoundingClientRect();
54
+ if (!(clientX >= box.left && clientX <= box.right && clientY >= box.top && clientY <= box.bottom)) return [];
55
+ return this.markersInReach({
56
+ x: clientX - box.left,
57
+ y: clientY - box.top
58
+ }, box.width).flatMap((index) => {
59
+ const marker = this.#markers[index];
60
+ return marker ? [{ ...marker }] : [];
61
+ });
62
+ }
51
63
  repaint() {
52
64
  this.#surface?.rebuild();
53
65
  }
@@ -75,9 +87,6 @@ var SonicWaveElement = class extends SonicValueElement {
75
87
  this.render();
76
88
  this.#surface.setFill(this.fill);
77
89
  }
78
- focusTarget() {
79
- return this.control;
80
- }
81
90
  markerList() {
82
91
  return this.#markers;
83
92
  }
@@ -104,6 +113,9 @@ var SonicWaveElement = class extends SonicValueElement {
104
113
  this.#current = current;
105
114
  this.dispatchEvent(new Event("sonic-marker", { bubbles: true }));
106
115
  }
116
+ stateTarget() {
117
+ return this.control;
118
+ }
107
119
  surface() {
108
120
  return this.#surface;
109
121
  }
@@ -15,6 +15,7 @@ import { bindZoom, bindZoomKeys } from "../lib/zoom-gesture.js";
15
15
  const defaultZoom = 70;
16
16
  const defaultZoomMin = 20;
17
17
  const defaultZoomMax = 280;
18
+ const lineReachPx = 4;
18
19
  const renderWaveform = template(`
19
20
  <div class="sonic-waveform" role="slider" tabindex="0">
20
21
  <canvas class="sonic-waveform-canvas" aria-hidden="true"></canvas>
@@ -201,10 +202,21 @@ var SonicWaveform = class extends SonicWaveElement {
201
202
  super.attributeChangedCallback(name);
202
203
  if (name === "zoomable") this.#zoomGesture?.sync();
203
204
  }
205
+ /** Viewport x of a time as last drawn; not clamped to the window */
206
+ clientXOf(value) {
207
+ const { left, width } = this.canvas.getBoundingClientRect();
208
+ return left + this.#drawnX(value, width);
209
+ }
204
210
  connectedCallback() {
205
211
  this.upgradeProperties("bands", "peaks", "pending", "pendingDelay", "playing", "playbackRate", "readTime", "reducedMotion", "renderLabel", "requestPeaks", "zoom", "zoomable", "zoomMax", "zoomMin");
206
212
  super.connectedCallback();
207
213
  }
214
+ /** The time at a viewport x as last drawn */
215
+ valueFromPoint(clientX, _clientY) {
216
+ const { left, width } = this.canvas.getBoundingClientRect();
217
+ const { startSeconds, zoom } = this.#drawnWindow(width);
218
+ return clamp(startSeconds + (clientX - left) / zoom, ...this.mapping().bounds);
219
+ }
208
220
  connect(signal) {
209
221
  super.connect(signal);
210
222
  const zoomTarget = {
@@ -231,9 +243,18 @@ var SonicWaveform = class extends SonicWaveElement {
231
243
  surface?.requestFrame();
232
244
  if (surface?.isVisible === false || !this.#drawn) this.showCurrentMarker(this.#sourceSeconds());
233
245
  }
246
+ focusTarget() {
247
+ return this.control;
248
+ }
234
249
  isEmpty() {
235
250
  return !this.#peaks || this.#peaks.samples.length === 0;
236
251
  }
252
+ markersInReach({ x }, widthPx) {
253
+ return this.markerList().map(({ start }, index) => ({
254
+ distance: Math.abs(this.#drawnX(start, widthPx) - x),
255
+ index
256
+ })).filter(({ distance }) => distance <= lineReachPx).toSorted((first, second) => first.distance - second.distance).map(({ index }) => index);
257
+ }
237
258
  paint(context, { frameMs, isDirty, isRebuilt, look, size }) {
238
259
  const held = this.#held();
239
260
  const isPaged = look.isReducedMotion && this.reducedMotion === "page";
@@ -258,7 +279,8 @@ var SonicWaveform = class extends SonicWaveElement {
258
279
  const { seconds, view } = timeline;
259
280
  this.#drawn = {
260
281
  seconds,
261
- startSeconds: view.startSeconds
282
+ startSeconds: view.startSeconds,
283
+ zoom: this.zoom
262
284
  };
263
285
  placeLine(this.#playhead, timeline.playheadAt);
264
286
  placeLine(this.#ghost, timeline.ghostAt);
@@ -281,7 +303,10 @@ var SonicWaveform = class extends SonicWaveElement {
281
303
  }, {
282
304
  bands: this.#tint(isRebuilt),
283
305
  kindColours: this.#kindColours,
284
- markers: this.markerList(),
306
+ markers: this.markerList().map((marker) => ({
307
+ ...marker,
308
+ start: clamp(marker.start, ...range)
309
+ })),
285
310
  peaks: this.#peaks,
286
311
  range
287
312
  });
@@ -311,6 +336,17 @@ var SonicWaveform = class extends SonicWaveElement {
311
336
  scrubsKeyRepeat() {
312
337
  return true;
313
338
  }
339
+ #drawnWindow(widthPx) {
340
+ const zoom = this.#drawn?.zoom ?? this.zoom;
341
+ return {
342
+ startSeconds: this.#drawn?.startSeconds ?? this.value - widthPx / zoom / 2,
343
+ zoom
344
+ };
345
+ }
346
+ #drawnX(value, widthPx) {
347
+ const { startSeconds, zoom } = this.#drawnWindow(widthPx);
348
+ return (clamp(value, ...this.mapping().bounds) - startSeconds) * zoom;
349
+ }
314
350
  #duePending() {
315
351
  const due = dueRegions(this.#pending, this.#pendingListedMs, [performance.now(), this.pendingDelay]);
316
352
  if (due.length < this.#pending.length) this.surface()?.requestFrame();