@xsynaptic/sonic-ui 0.17.0 → 0.18.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 (125) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +112 -19
  3. package/dist/default/dat.js +1 -0
  4. package/dist/default/elements/button.js +2 -0
  5. package/dist/default/elements/dial.js +2 -0
  6. package/dist/default/elements/display-element.js +9 -7
  7. package/dist/default/elements/envelope.js +31 -72
  8. package/dist/default/elements/field-gesture.js +81 -0
  9. package/dist/default/elements/meter.js +4 -0
  10. package/dist/default/elements/position-group.js +42 -26
  11. package/dist/default/elements/radio-group.js +59 -56
  12. package/dist/default/elements/scale.js +3 -3
  13. package/dist/default/elements/slider.js +11 -13
  14. package/dist/default/elements/split.js +14 -13
  15. package/dist/default/elements/switch.js +31 -46
  16. package/dist/default/elements/toggle.js +20 -44
  17. package/dist/default/elements/value-element.js +65 -91
  18. package/dist/default/elements/value-gestures.js +22 -22
  19. package/dist/default/elements/wave-element.js +9 -12
  20. package/dist/default/elements/waveform.js +42 -241
  21. package/dist/default/elements/wavestrip.js +10 -5
  22. package/dist/default/elements/xy.js +67 -112
  23. package/dist/default/lib/canvas-surface.js +25 -0
  24. package/dist/default/lib/crossfade.js +1 -0
  25. package/dist/default/lib/envelope-curve.js +1 -0
  26. package/dist/default/lib/peaks-request.js +44 -0
  27. package/dist/default/lib/positions.js +58 -0
  28. package/dist/default/lib/scrub.js +12 -0
  29. package/dist/default/lib/value-model.js +62 -0
  30. package/dist/default/lib/waveform-scene.js +108 -0
  31. package/dist/default/lib/zoom-gesture.js +97 -1
  32. package/dist/dev/dat.d.ts +1 -0
  33. package/dist/dev/dat.js +1 -0
  34. package/dist/dev/dat.js.map +1 -1
  35. package/dist/dev/elements/button.d.ts +2 -0
  36. package/dist/dev/elements/button.js +2 -0
  37. package/dist/dev/elements/button.js.map +1 -1
  38. package/dist/dev/elements/dial.d.ts +2 -0
  39. package/dist/dev/elements/dial.js +2 -0
  40. package/dist/dev/elements/dial.js.map +1 -1
  41. package/dist/dev/elements/display-element.d.ts +5 -3
  42. package/dist/dev/elements/display-element.js +9 -7
  43. package/dist/dev/elements/display-element.js.map +1 -1
  44. package/dist/dev/elements/envelope.js +31 -72
  45. package/dist/dev/elements/envelope.js.map +1 -1
  46. package/dist/dev/elements/field-gesture.js +82 -0
  47. package/dist/dev/elements/field-gesture.js.map +1 -0
  48. package/dist/dev/elements/meter.d.ts +10 -5
  49. package/dist/dev/elements/meter.js +4 -0
  50. package/dist/dev/elements/meter.js.map +1 -1
  51. package/dist/dev/elements/position-group.d.ts +12 -6
  52. package/dist/dev/elements/position-group.js +42 -26
  53. package/dist/dev/elements/position-group.js.map +1 -1
  54. package/dist/dev/elements/radio-group.d.ts +11 -7
  55. package/dist/dev/elements/radio-group.js +59 -56
  56. package/dist/dev/elements/radio-group.js.map +1 -1
  57. package/dist/dev/elements/scale.js +3 -3
  58. package/dist/dev/elements/scale.js.map +1 -1
  59. package/dist/dev/elements/slider.d.ts +4 -2
  60. package/dist/dev/elements/slider.js +11 -13
  61. package/dist/dev/elements/slider.js.map +1 -1
  62. package/dist/dev/elements/split.d.ts +1 -0
  63. package/dist/dev/elements/split.js +14 -13
  64. package/dist/dev/elements/split.js.map +1 -1
  65. package/dist/dev/elements/switch.d.ts +2 -2
  66. package/dist/dev/elements/switch.js +31 -46
  67. package/dist/dev/elements/switch.js.map +1 -1
  68. package/dist/dev/elements/toggle.js +20 -44
  69. package/dist/dev/elements/toggle.js.map +1 -1
  70. package/dist/dev/elements/value-element.d.ts +5 -5
  71. package/dist/dev/elements/value-element.js +65 -91
  72. package/dist/dev/elements/value-element.js.map +1 -1
  73. package/dist/dev/elements/value-gestures.js +22 -22
  74. package/dist/dev/elements/value-gestures.js.map +1 -1
  75. package/dist/dev/elements/wave-element.d.ts +6 -4
  76. package/dist/dev/elements/wave-element.js +9 -12
  77. package/dist/dev/elements/wave-element.js.map +1 -1
  78. package/dist/dev/elements/waveform.d.ts +19 -21
  79. package/dist/dev/elements/waveform.js +42 -241
  80. package/dist/dev/elements/waveform.js.map +1 -1
  81. package/dist/dev/elements/wavestrip.d.ts +5 -1
  82. package/dist/dev/elements/wavestrip.js +10 -5
  83. package/dist/dev/elements/wavestrip.js.map +1 -1
  84. package/dist/dev/elements/xy.js +67 -112
  85. package/dist/dev/elements/xy.js.map +1 -1
  86. package/dist/dev/index.d.ts +2 -1
  87. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  88. package/dist/dev/lib/canvas-surface.js +25 -0
  89. package/dist/dev/lib/canvas-surface.js.map +1 -1
  90. package/dist/dev/lib/crossfade.d.ts +1 -0
  91. package/dist/dev/lib/crossfade.js +1 -0
  92. package/dist/dev/lib/crossfade.js.map +1 -1
  93. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  94. package/dist/dev/lib/envelope-curve.js +1 -0
  95. package/dist/dev/lib/envelope-curve.js.map +1 -1
  96. package/dist/dev/lib/peaks-request.d.ts +5 -0
  97. package/dist/dev/lib/peaks-request.js +45 -0
  98. package/dist/dev/lib/peaks-request.js.map +1 -0
  99. package/dist/dev/lib/positions.d.ts +7 -0
  100. package/dist/dev/lib/positions.js +59 -0
  101. package/dist/dev/lib/positions.js.map +1 -0
  102. package/dist/dev/lib/scrub.d.ts +8 -0
  103. package/dist/dev/lib/scrub.js +13 -0
  104. package/dist/dev/lib/scrub.js.map +1 -0
  105. package/dist/dev/lib/time-regions.d.ts +1 -0
  106. package/dist/dev/lib/time-regions.js.map +1 -1
  107. package/dist/dev/lib/units.d.ts +8 -0
  108. package/dist/dev/lib/value-model.js +63 -0
  109. package/dist/dev/lib/value-model.js.map +1 -0
  110. package/dist/dev/lib/waveform-buckets.d.ts +1 -0
  111. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  112. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  113. package/dist/dev/lib/waveform-scene.js +109 -0
  114. package/dist/dev/lib/waveform-scene.js.map +1 -0
  115. package/dist/dev/lib/zoom-gesture.js +97 -1
  116. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  117. package/dist/styles/button.css +16 -6
  118. package/dist/styles/waveform.css +2 -1
  119. package/package.json +1 -1
  120. package/dist/default/lib/asked-value.js +0 -21
  121. package/dist/default/lib/fill-watch.js +0 -29
  122. package/dist/dev/lib/asked-value.js +0 -22
  123. package/dist/dev/lib/asked-value.js.map +0 -1
  124. package/dist/dev/lib/fill-watch.js +0 -30
  125. package/dist/dev/lib/fill-watch.js.map +0 -1
@@ -1,8 +1,13 @@
1
1
  import { writeAttribute } from "../lib/write-attribute.js";
2
2
  import { bindHold } from "../lib/hold.js";
3
+ import { isMomentaryAt, pressTarget, restOf } from "../lib/positions.js";
3
4
  import { SonicRadioGroupElement, optionValue } from "./radio-group.js";
4
5
 
5
6
  //#region src/elements/position-group.ts
7
+ const latching = {
8
+ isDisabled: false,
9
+ isMomentary: false
10
+ };
6
11
  var SonicPositionGroupElement = class extends SonicRadioGroupElement {
7
12
  static observedAttributes = [
8
13
  ...SonicRadioGroupElement.observedAttributes,
@@ -56,21 +61,22 @@ var SonicPositionGroupElement = class extends SonicRadioGroupElement {
56
61
  along(event) {
57
62
  return this.orientation === "vertical" ? event.clientY : event.clientX;
58
63
  }
59
- checkedIndex(options) {
60
- return options.findIndex((option) => optionValue(option) === this.value);
64
+ checkedIndex() {
65
+ if (this.isBare()) return this.checked ? this.onIndex() : 1 - this.onIndex();
66
+ return this.options().findIndex((option) => optionValue(option) === this.value);
61
67
  }
62
- chooseByClick(option) {
63
- super.chooseByClick(option);
64
- if (this.isMomentary(option)) this.springBack();
68
+ chooseByClick(index) {
69
+ super.chooseByClick(index);
70
+ if (this.isMomentary(index)) this.springBack();
65
71
  }
66
- chooseByKey(option, event) {
67
- if (!this.isMomentary(option)) {
68
- super.chooseByKey(option, event);
72
+ chooseByKey(index, event) {
73
+ if (!this.isMomentary(index)) {
74
+ super.chooseByKey(index, event);
69
75
  return;
70
76
  }
71
77
  if (event.repeat) return;
72
- this.hold(option, event.key);
73
- option.focus();
78
+ this.hold(index, event.key);
79
+ this.options()[index]?.focus();
74
80
  }
75
81
  connect(signal) {
76
82
  this.upgradeProperties("defaultChecked", "checked", "orientation");
@@ -90,23 +96,33 @@ var SonicPositionGroupElement = class extends SonicRadioGroupElement {
90
96
  focusTarget() {
91
97
  return this.isBare() ? this.bare : super.focusTarget();
92
98
  }
93
- hold(option, by) {
94
- if (this.#holding?.hold(by) === true) this.select(option);
99
+ hold(index, by) {
100
+ if (this.#holding?.hold(by) === true) this.select(index);
95
101
  }
96
102
  isBare() {
97
103
  return this.options().length === 0;
98
104
  }
99
- isMomentary(option) {
100
- const options = this.options();
101
- return (option === options[0] || option === options.at(-1)) && options.length > 1 && option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-momentary") === true;
105
+ isChecked(index) {
106
+ const option = this.options()[index];
107
+ return option ? optionValue(option) === this.value : index === this.checkedIndex();
108
+ }
109
+ isMomentary(index) {
110
+ return isMomentaryAt(this.positions(), index);
102
111
  }
103
112
  isWrapping() {
104
113
  return false;
105
114
  }
106
- pressTarget(option) {
107
- const options = this.options();
108
- const index = this.checkedIndex(options);
109
- return (options.length === 2 && index >= 0 ? options[1 - index] : void 0) ?? option;
115
+ onIndex() {
116
+ return this.orientation === "vertical" ? 0 : 1;
117
+ }
118
+ positions() {
119
+ return this.isBare() ? [latching, latching] : super.positions();
120
+ }
121
+ pressedIndex(target) {
122
+ return target === this.bare ? 0 : this.indexOf(target);
123
+ }
124
+ pressTarget(index) {
125
+ return pressTarget(this.positions(), this.checkedIndex(), index);
110
126
  }
111
127
  releaseHold() {
112
128
  this.#holding?.release();
@@ -128,21 +144,21 @@ var SonicPositionGroupElement = class extends SonicRadioGroupElement {
128
144
  if (this.isBare()) this.checked = state === "true";
129
145
  else super.restoreState(state);
130
146
  }
147
+ select(index) {
148
+ if (this.isBare()) this.setChecked(index === this.onIndex());
149
+ else super.select(index);
150
+ }
131
151
  setChecked(isChecked) {
132
152
  if (isChecked === this.checked) return;
133
153
  this.checked = isChecked;
134
154
  this.dispatchEvent(new Event("change", { bubbles: true }));
135
155
  }
136
156
  springBack() {
137
- const options = this.options();
138
- const index = this.checkedIndex(options);
139
- const held = options[index];
140
- if (!held || !this.isMomentary(held)) return;
141
- const rest = options[index === 0 ? 1 : index - 1];
142
- if (!rest) return;
157
+ const rest = restOf(this.positions(), this.checkedIndex());
158
+ if (rest === void 0) return;
143
159
  const isFocused = this.group.contains(this.ownerDocument.activeElement);
144
160
  this.select(rest);
145
- if (isFocused) rest.focus();
161
+ if (isFocused) this.options()[rest]?.focus();
146
162
  }
147
163
  #renderBare() {
148
164
  const group = this.group;
@@ -1,4 +1,3 @@
1
- import { clamp } from "../lib/math.js";
2
1
  import { writeAttribute } from "../lib/write-attribute.js";
3
2
  import { SonicFormElement } from "./form-element.js";
4
3
  import { copyNode } from "../lib/copy-node.js";
@@ -6,14 +5,9 @@ import { bindKeyPress } from "../lib/key-press.js";
6
5
  import { placeChildren } from "../lib/render.js";
7
6
  import { mirrorChildren } from "../lib/mirror-children.js";
8
7
  import { bindDrag } from "../lib/pointer-drag.js";
8
+ import { keyTarget, stepFrom } from "../lib/positions.js";
9
9
 
10
10
  //#region src/elements/radio-group.ts
11
- const keySteps = /* @__PURE__ */ new Map([
12
- ["ArrowDown", 1],
13
- ["ArrowLeft", -1],
14
- ["ArrowRight", 1],
15
- ["ArrowUp", -1]
16
- ]);
17
11
  function isInside(box, x, y) {
18
12
  return x >= box.left && x < box.right && y >= box.top && y < box.bottom;
19
13
  }
@@ -38,14 +32,14 @@ var SonicRadioGroupElement = class extends SonicFormElement {
38
32
  if (name === "value") this.#value = this.getAttribute("value") ?? "";
39
33
  this.render();
40
34
  }
41
- chooseByClick(option) {
42
- this.select(option);
35
+ chooseByClick(index) {
36
+ this.select(index);
43
37
  }
44
- chooseByKey(option, _event) {
45
- this.select(option);
46
- option.focus();
38
+ chooseByKey(index, _event) {
39
+ this.select(index);
40
+ this.options()[index]?.focus();
47
41
  }
48
- claimPress(_option, _event) {
42
+ claimPress(_index, _event) {
49
43
  return false;
50
44
  }
51
45
  connect(signal) {
@@ -61,16 +55,19 @@ var SonicRadioGroupElement = class extends SonicFormElement {
61
55
  }, signal);
62
56
  group.addEventListener("click", (event) => {
63
57
  if (event instanceof PointerEvent && event.pointerType !== "") return;
64
- const option = this.optionOf(event.target);
65
- if (option) this.chooseByClick(option);
58
+ const index = this.indexOf(event.target);
59
+ if (index !== -1) this.chooseByClick(index);
66
60
  }, { signal });
67
61
  this.#bindPress(group, signal);
68
62
  bindKeyPress(group, signal);
69
63
  group.addEventListener("keydown", (event) => {
70
64
  if (event.defaultPrevented) return;
71
- const option = this.optionOf(event.target);
72
- const next = option && this.#keyTarget(option, event.key);
73
- if (!next) return;
65
+ const index = this.indexOf(event.target);
66
+ const next = index === -1 ? void 0 : keyTarget(this.positions(), index, {
67
+ isWrapping: this.isWrapping(),
68
+ key: event.key
69
+ });
70
+ if (next === void 0) return;
74
71
  event.preventDefault();
75
72
  this.chooseByKey(next, event);
76
73
  }, { signal });
@@ -78,11 +75,15 @@ var SonicRadioGroupElement = class extends SonicFormElement {
78
75
  focusTarget() {
79
76
  return this.options().find((option) => option.tabIndex === 0);
80
77
  }
78
+ indexOf(target) {
79
+ const option = this.optionOf(target);
80
+ return option ? this.options().indexOf(option) : -1;
81
+ }
81
82
  isMirrored(child) {
82
83
  return child instanceof Element && child.matches("[data-sonic-value]");
83
84
  }
84
- isOptionDisabled(option) {
85
- return option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-disabled") === true;
85
+ isOptionDisabled(index) {
86
+ return this.positions()[index]?.isDisabled !== false;
86
87
  }
87
88
  isWrapping() {
88
89
  return true;
@@ -98,14 +99,27 @@ var SonicRadioGroupElement = class extends SonicFormElement {
98
99
  placeCopies(copies) {
99
100
  this.#copyOptions(copies);
100
101
  }
101
- releaseTarget(option) {
102
- return option;
102
+ positions() {
103
+ return this.options().map((option) => {
104
+ const child = option.querySelector("[data-sonic-value]");
105
+ return {
106
+ isDisabled: child?.hasAttribute("data-sonic-disabled") === true,
107
+ isMomentary: child?.hasAttribute("data-sonic-momentary") === true
108
+ };
109
+ });
110
+ }
111
+ refocusAt(index) {
112
+ if (this.group.matches(":focus-within")) this.options()[index]?.focus();
113
+ }
114
+ releaseTarget(index) {
115
+ return index;
103
116
  }
104
117
  render() {
105
118
  if (!this.isBound()) return;
106
119
  const options = this.options();
120
+ const positions = this.positions();
107
121
  const checked = options.find((option) => optionValue(option) === this.#value);
108
- const enabled = this.isDisabled() ? [] : options.filter((option) => !this.isOptionDisabled(option));
122
+ const enabled = this.isDisabled() ? [] : options.filter((_option, index) => positions[index]?.isDisabled === false);
109
123
  const focusable = enabled.find((option) => option === checked) ?? enabled[0];
110
124
  for (const option of options) {
111
125
  writeAttribute(option, "disabled", enabled.includes(option) ? void 0 : "");
@@ -118,45 +132,42 @@ var SonicRadioGroupElement = class extends SonicFormElement {
118
132
  restoreState(state) {
119
133
  this.value = state;
120
134
  }
121
- select(option) {
122
- const next = optionValue(option);
135
+ select(index) {
136
+ const option = this.options()[index];
137
+ const next = option && optionValue(option);
123
138
  if (next === void 0 || next === this.#value) return;
124
139
  this.value = next;
125
140
  this.dispatchEvent(new Event("change", { bubbles: true }));
126
141
  }
127
142
  stepFrom(from, step) {
128
- const options = this.options();
129
- let index = from;
130
- for (const _ of options) {
131
- const next = index + step;
132
- index = this.isWrapping() ? (next + options.length) % options.length : clamp(next, 0, options.length - 1);
133
- const option = options[index];
134
- if (option && !this.isOptionDisabled(option)) return option;
135
- }
143
+ return stepFrom(this.positions(), from, {
144
+ isWrapping: this.isWrapping(),
145
+ step
146
+ });
136
147
  }
137
148
  #bindPress(group, signal) {
138
149
  bindDrag(group, {
139
150
  grab: (event) => {
140
- const option = this.optionOf(event.target);
141
- if (!option || this.isDisabled() || this.isOptionDisabled(option)) return;
142
- if (this.claimPress(option, event)) return;
151
+ const index = this.indexOf(event.target);
152
+ if (index === -1 || this.isDisabled() || this.isOptionDisabled(index)) return;
153
+ if (this.claimPress(index, event)) return;
143
154
  const boxes = this.#optionBoxes();
144
- this.#markPressed(option);
155
+ this.#markPressed(index);
145
156
  return {
146
157
  boxes,
147
- option
158
+ index
148
159
  };
149
160
  },
150
161
  lift: (press) => {
151
- if (!press.option || this.isDisabled()) return;
152
- const option = this.releaseTarget(press.option);
153
- if (!option) return;
154
- this.select(option);
155
- if (group.matches(":focus-within")) option.focus();
162
+ if (press.index === void 0 || this.isDisabled()) return;
163
+ const index = this.releaseTarget(press.index);
164
+ if (index === void 0) return;
165
+ this.select(index);
166
+ this.refocusAt(index);
156
167
  },
157
168
  move: (press, event) => {
158
- press.option = press.boxes.find(([, box]) => isInside(box, event.clientX, event.clientY))?.[0];
159
- this.#markPressed(press.option);
169
+ press.index = press.boxes.find(([, box]) => isInside(box, event.clientX, event.clientY))?.[0];
170
+ this.#markPressed(press.index);
160
171
  },
161
172
  release: () => {
162
173
  this.#markPressed(void 0);
@@ -180,20 +191,12 @@ var SonicRadioGroupElement = class extends SonicFormElement {
180
191
  this.render();
181
192
  if (focused) this.#refocus(focused, focusedValue);
182
193
  }
183
- #keyTarget(option, key) {
184
- const options = this.options();
185
- const isEnabled = (other) => !this.isOptionDisabled(other);
186
- if (key === "Home") return options.find((other) => isEnabled(other));
187
- if (key === "End") return options.findLast((other) => isEnabled(other));
188
- const step = keySteps.get(key);
189
- if (step === void 0) return void 0;
190
- return this.stepFrom(options.indexOf(option), step) ?? option;
191
- }
192
194
  #markPressed(pressed) {
193
- for (const option of this.options()) option.toggleAttribute("data-sonic-pressed", option === pressed);
195
+ for (const [index, option] of this.options().entries()) option.toggleAttribute("data-sonic-pressed", index === pressed);
194
196
  }
195
197
  #optionBoxes() {
196
- return this.options().filter((option) => !this.isOptionDisabled(option)).map((option) => [option, option.getBoundingClientRect()]);
198
+ const positions = this.positions();
199
+ return this.options().flatMap((option, index) => positions[index]?.isDisabled === false ? [[index, option.getBoundingClientRect()]] : []);
197
200
  }
198
201
  #refocus(focused, value) {
199
202
  const options = this.options();
@@ -17,9 +17,9 @@ function scaleMark(original) {
17
17
  return mark;
18
18
  }
19
19
  function bindScale(element, { control, marks }, signal) {
20
- const { mapping, watch } = linkValue(element);
20
+ const { model } = linkValue(element);
21
21
  const render = () => {
22
- const { proportionOf } = mapping();
22
+ const { proportionOf } = model.mapping();
23
23
  for (const mark of marks.children) {
24
24
  if (!(mark instanceof HTMLElement || mark instanceof SVGElement)) continue;
25
25
  const at = String(proportionOf(scaleValue(mark)));
@@ -36,7 +36,7 @@ function bindScale(element, { control, marks }, signal) {
36
36
  render();
37
37
  }
38
38
  }, signal);
39
- signal.addEventListener("abort", watch(render), { once: true });
39
+ signal.addEventListener("abort", model.watch(render), { once: true });
40
40
  }
41
41
 
42
42
  //#endregion
@@ -3,6 +3,7 @@ import { requireChild, template } from "../lib/render.js";
3
3
  import { createModulation } from "./modulation.js";
4
4
  import { SonicValueElement } from "./value-element.js";
5
5
  import { bindScale } from "./scale.js";
6
+ import { scrubRegions } from "../lib/scrub.js";
6
7
  import { sortedRegions } from "../lib/time-regions.js";
7
8
 
8
9
  //#region src/elements/slider.ts
@@ -39,6 +40,7 @@ var SonicSlider = class extends SonicValueElement {
39
40
  "scrub",
40
41
  "spring"
41
42
  ];
43
+ /** Loaded spans, in the slider's own values */
42
44
  get buffered() {
43
45
  return this.#buffered.map(([start, end]) => [start, end]);
44
46
  }
@@ -52,12 +54,14 @@ var SonicSlider = class extends SonicValueElement {
52
54
  set groovePress(gesture) {
53
55
  this.reflect("groove-press", gesture);
54
56
  }
57
+ /** How far a modulation source can push the value, in value units; signed */
55
58
  get modulation() {
56
59
  return this.numberAttribute("modulation", 0);
57
60
  }
58
61
  set modulation(amount) {
59
62
  this.reflect("modulation", amount);
60
63
  }
64
+ /** Where the value is right now with modulation applied; a property only, cheap to write every frame, fires nothing */
61
65
  get modulationValue() {
62
66
  return this.#modulation.value();
63
67
  }
@@ -146,11 +150,7 @@ var SonicSlider = class extends SonicValueElement {
146
150
  focusTarget() {
147
151
  return this.#slider;
148
152
  }
149
- heldWrite(next) {
150
- super.heldWrite(next);
151
- if (this.scrub) this.render();
152
- }
153
- holdChanged() {
153
+ scrubChanged() {
154
154
  if (this.scrub) this.render();
155
155
  }
156
156
  scrubsKeyRepeat() {
@@ -199,18 +199,16 @@ var SonicSlider = class extends SonicValueElement {
199
199
  }
200
200
  #renderScrub(slider) {
201
201
  const { style } = slider;
202
- const { proportionOf } = this.mapping();
203
- const isHeld = this.scrub && this.heldFrom() !== void 0;
204
- const played = proportionOf(this.playback());
205
- const scrubbed = proportionOf(this.value);
206
- if (isHeld) style.setProperty("--_sonic-slider-played", String(played));
202
+ const state = this.scrubState();
203
+ const { played, scrub } = scrubRegions(state, this.value, this.mapping());
204
+ if (this.scrub && state.from !== void 0) style.setProperty("--_sonic-slider-played", String(played));
207
205
  else style.removeProperty("--_sonic-slider-played");
208
- if (played === scrubbed || !isHeld || !this.isRevealed()) {
206
+ if (scrub === void 0 || !this.scrub) {
209
207
  style.removeProperty("--_sonic-slider-scrub-region");
210
208
  return;
211
209
  }
212
- const from = grooveStop(Math.min(played, scrubbed));
213
- const to = grooveStop(Math.max(played, scrubbed));
210
+ const from = grooveStop(Math.min(played, scrub));
211
+ const to = grooveStop(Math.max(played, scrub));
214
212
  style.setProperty("--_sonic-slider-scrub-region", `linear-gradient(var(--_sonic-slider-toward), transparent ${from}, var(--_sonic-slider-scrub) ${from} ${to}, transparent ${to})`);
215
213
  }
216
214
  };
@@ -22,7 +22,7 @@ function gridLimits(mapping, [low, high]) {
22
22
  return [bottom < low - gridSlop ? mapping.keyTarget("ArrowRight", bottom) ?? bottom : bottom, top > high + gridSlop ? mapping.keyTarget("ArrowLeft", top) ?? top : top];
23
23
  }
24
24
  function modelOf(member, value, isFree) {
25
- const mapping = member.link.mapping();
25
+ const mapping = member.link.model.mapping();
26
26
  const [first, last] = mapping.bounds;
27
27
  return {
28
28
  isFree,
@@ -46,6 +46,7 @@ var SonicSplit = class extends SonicElement {
46
46
  set mode(name) {
47
47
  this.reflect("mode", name);
48
48
  }
49
+ /** What the members add up to; unset, it is their sum when they connect */
49
50
  get total() {
50
51
  return this.optionalNumberAttribute("total");
51
52
  }
@@ -105,7 +106,7 @@ var SonicSplit = class extends SonicElement {
105
106
  }
106
107
  #compensate() {
107
108
  this.#isQueued = false;
108
- for (const mover of this.#members) if (mover.link.value() !== mover.seen) this.#move(mover);
109
+ for (const mover of this.#members) if (mover.link.model.value !== mover.seen) this.#move(mover);
109
110
  }
110
111
  #find() {
111
112
  const found = [];
@@ -121,18 +122,18 @@ var SonicSplit = class extends SonicElement {
121
122
  }
122
123
  #join(element) {
123
124
  const link = linkValue(element);
124
- const unwatch = link.watch(() => {
125
+ const unwatch = link.model.watch(() => {
125
126
  if (this.#isWriting || this.#isQueued) return;
126
127
  this.#isQueued = true;
127
128
  queueMicrotask(() => {
128
129
  if (this.#isQueued) this.#compensate();
129
130
  });
130
131
  });
131
- link.land((target, direction) => this.#land(element, target, direction));
132
+ link.model.setLanding((target, direction) => this.#land(element, target, direction));
132
133
  return {
133
134
  element,
134
135
  link,
135
- seen: link.value(),
136
+ seen: link.model.value,
136
137
  unwatch
137
138
  };
138
139
  }
@@ -155,7 +156,7 @@ var SonicSplit = class extends SonicElement {
155
156
  for (const [index, member] of this.#members.entries()) {
156
157
  const model = this.#model(values, member);
157
158
  const value = values[index] ?? member.seen;
158
- member.link.limit(isLocked(member) ? [value, value] : gridLimits(member.link.mapping(), splitLimits(model, index, this.#total)));
159
+ member.link.model.setLimit(isLocked(member) ? [value, value] : gridLimits(member.link.model.mapping(), splitLimits(model, index, this.#total)));
159
160
  }
160
161
  }
161
162
  #model(values, mover) {
@@ -177,7 +178,7 @@ var SonicSplit = class extends SonicElement {
177
178
  total: this.#total
178
179
  }, {
179
180
  index,
180
- target: mover.link.value()
181
+ target: mover.link.model.value
181
182
  }), mover);
182
183
  this.#limit();
183
184
  }
@@ -187,7 +188,7 @@ var SonicSplit = class extends SonicElement {
187
188
  const snapshot = this.#snapshot;
188
189
  if (snapshot?.mover.element !== event.target) return;
189
190
  this.#snapshot = void 0;
190
- const changed = this.#members.filter((member, index) => member !== snapshot.mover && member.link.value() !== snapshot.values[index]);
191
+ const changed = this.#members.filter((member, index) => member !== snapshot.mover && member.link.model.value !== snapshot.values[index]);
191
192
  queueMicrotask(() => {
192
193
  this.#isWriting = true;
193
194
  try {
@@ -213,8 +214,8 @@ var SonicSplit = class extends SonicElement {
213
214
  #unbind() {
214
215
  for (const { link, unwatch } of this.#members) {
215
216
  unwatch();
216
- link.land(void 0);
217
- link.limit(void 0);
217
+ link.model.setLanding(void 0);
218
+ link.model.setLimit(void 0);
218
219
  }
219
220
  this.#members = [];
220
221
  this.#snapshot = void 0;
@@ -234,14 +235,14 @@ var SonicSplit = class extends SonicElement {
234
235
  try {
235
236
  for (const [index, member] of this.#members.entries()) {
236
237
  const value = values[index];
237
- if (value === void 0 || value === member.link.value()) continue;
238
- member.link.limit(void 0);
238
+ if (value === void 0 || value === member.link.model.value) continue;
239
+ member.link.model.setLimit(void 0);
239
240
  member.link.input(value, member === mover);
240
241
  }
241
242
  } finally {
242
243
  this.#isWriting = false;
243
244
  }
244
- for (const member of this.#members) member.seen = member.link.value();
245
+ for (const member of this.#members) member.seen = member.link.model.value;
245
246
  }
246
247
  };
247
248
 
@@ -1,6 +1,7 @@
1
1
  import { requireChild, template } from "../lib/render.js";
2
2
  import { bindDrag } from "../lib/pointer-drag.js";
3
- import { isUnder, optionValue } from "./radio-group.js";
3
+ import { soleStep } from "../lib/positions.js";
4
+ import { isUnder } from "./radio-group.js";
4
5
  import { SonicPositionGroupElement } from "./position-group.js";
5
6
 
6
7
  //#region src/elements/switch.ts
@@ -23,9 +24,9 @@ var SonicSwitch = class extends SonicPositionGroupElement {
23
24
  group = renderSwitch();
24
25
  #bat = requireChild(this.group, ".sonic-switch-bat", HTMLSpanElement);
25
26
  #isClickSwallowed = false;
26
- claimPress(option, event) {
27
+ claimPress(index, event) {
27
28
  if (this.#isOnBat(event)) return true;
28
- const target = this.pressTarget(option);
29
+ const target = this.pressTarget(index);
29
30
  if (this.isOptionDisabled(target) || !this.isMomentary(target)) return false;
30
31
  this.group.setPointerCapture(event.pointerId);
31
32
  this.hold(target, event.pointerId);
@@ -45,20 +46,14 @@ var SonicSwitch = class extends SonicPositionGroupElement {
45
46
  }, { signal });
46
47
  this.#bindBat(group, signal);
47
48
  }
48
- releaseTarget(option) {
49
- const target = this.pressTarget(option);
49
+ releaseTarget(index) {
50
+ const target = this.pressTarget(index);
50
51
  return this.isMomentary(target) || this.isOptionDisabled(target) ? void 0 : target;
51
52
  }
52
53
  render() {
53
54
  if (!this.isBound()) return;
54
55
  super.render();
55
- const options = this.options();
56
- if (options.length > 0) {
57
- this.#renderThrow(this.checkedIndex(options));
58
- return;
59
- }
60
- const onSide = this.orientation === "horizontal" ? 1 : -1;
61
- this.group.style.setProperty("--_sonic-switch-at", String(this.checked ? onSide : -onSide));
56
+ this.#renderThrow(this.checkedIndex());
62
57
  }
63
58
  renderOption() {
64
59
  return renderPosition();
@@ -66,7 +61,7 @@ var SonicSwitch = class extends SonicPositionGroupElement {
66
61
  #bindBat(group, signal) {
67
62
  bindDrag(group, {
68
63
  grab: (event) => {
69
- if (!(event.target === this.bare || this.optionOf(event.target) !== void 0) || this.isDisabled() || !this.#isOnBat(event)) return;
64
+ if (!(this.pressedIndex(event.target) >= 0) || this.isDisabled() || !this.#isOnBat(event)) return;
70
65
  return {
71
66
  hasMoved: false,
72
67
  last: this.along(event),
@@ -87,51 +82,41 @@ var SonicSwitch = class extends SonicPositionGroupElement {
87
82
  }
88
83
  }, signal);
89
84
  }
90
- #hasSprungFrom(option) {
91
- if (!option || !this.isMomentary(option)) return false;
85
+ #hasSprungFrom(index) {
86
+ if (!this.isMomentary(index)) return false;
92
87
  this.releaseHold();
93
- return this.value !== optionValue(option);
88
+ return !this.isChecked(index);
94
89
  }
95
90
  #isOnBat(event) {
96
91
  return isUnder(this.#bat, event.clientX, event.clientY);
97
92
  }
98
93
  #pressBat(event) {
99
- const options = this.options();
100
- if (options.length === 0) {
101
- this.setChecked(!this.checked);
102
- return;
103
- }
104
- const box = this.#bat.getBoundingClientRect();
105
- const [start, size] = this.orientation === "horizontal" ? [box.left, box.width] : [box.top, box.height];
106
- const offset = (this.along(event) - start) / size - .5;
107
- const index = this.checkedIndex(options);
108
- const side = Math.abs(offset) < pressCenterZone ? 1 : Math.sign(offset);
109
- const toward = this.stepFrom(index, side);
110
- const option = toward && toward !== options[index] ? toward : this.stepFrom(index, -side);
111
- if (!option) return;
112
- this.chooseByClick(option);
113
- this.#refocus(options);
114
- }
115
- #refocus(options) {
116
- if (this.group.matches(":focus-within")) options[this.checkedIndex(options)]?.focus();
94
+ const from = this.checkedIndex();
95
+ const target = soleStep(this.positions(), from) ?? this.#sideStep(from, event);
96
+ if (target === void 0) return;
97
+ this.chooseByClick(target);
98
+ this.refocusAt(this.checkedIndex());
117
99
  }
118
100
  #renderThrow(index) {
119
- const count = this.options().length;
101
+ const count = this.positions().length;
120
102
  const at = index < 0 || count < 2 ? 0 : 2 * index / (count - 1) - 1;
121
103
  this.group.style.setProperty("--_sonic-switch-at", String(at));
122
104
  }
105
+ #sideStep(from, event) {
106
+ const box = this.#bat.getBoundingClientRect();
107
+ const [start, size] = this.orientation === "horizontal" ? [box.left, box.width] : [box.top, box.height];
108
+ const offset = (this.along(event) - start) / size - .5;
109
+ const side = Math.abs(offset) < pressCenterZone ? 1 : Math.sign(offset);
110
+ const toward = this.stepFrom(from, side);
111
+ return toward !== void 0 && toward !== from ? toward : this.stepFrom(from, -side);
112
+ }
123
113
  #throwBy(direction, pointerId) {
124
- const options = this.options();
125
- if (options.length === 0) {
126
- this.setChecked(direction === (this.orientation === "horizontal" ? 1 : -1));
127
- return;
128
- }
129
- const index = this.checkedIndex(options);
130
- const option = this.stepFrom(index, direction);
131
- if (!option || option === options[index] || this.#hasSprungFrom(options[index])) return;
132
- if (this.isMomentary(option)) this.hold(option, pointerId);
133
- else this.select(option);
134
- this.#refocus(options);
114
+ const from = this.checkedIndex();
115
+ const target = this.stepFrom(from, direction);
116
+ if (target === void 0 || target === from || this.#hasSprungFrom(from)) return;
117
+ if (this.isMomentary(target)) this.hold(target, pointerId);
118
+ else this.select(target);
119
+ this.refocusAt(this.checkedIndex());
135
120
  }
136
121
  };
137
122