@xsynaptic/sonic-ui 0.17.0 → 0.19.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 (178) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +122 -19
  3. package/dist/default/elements/button.js +2 -5
  4. package/dist/default/elements/dial.js +2 -7
  5. package/dist/default/elements/display-element.js +9 -12
  6. package/dist/default/elements/envelope.js +31 -78
  7. package/dist/default/elements/field-gesture.js +81 -0
  8. package/dist/default/elements/meter.js +4 -1
  9. package/dist/default/elements/number.js +0 -7
  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/segmented.js +0 -5
  14. package/dist/default/elements/slider.js +11 -20
  15. package/dist/default/elements/sonic-element.js +3 -1
  16. package/dist/default/elements/split.js +14 -13
  17. package/dist/default/elements/switch.js +31 -47
  18. package/dist/default/elements/toggle.js +20 -49
  19. package/dist/default/elements/value-element.js +72 -94
  20. package/dist/default/elements/value-gestures.js +22 -22
  21. package/dist/default/elements/wave-element.js +16 -20
  22. package/dist/default/elements/waveform.js +42 -241
  23. package/dist/default/elements/wavestrip.js +10 -5
  24. package/dist/default/elements/xy.js +67 -119
  25. package/dist/default/index.js +2 -1
  26. package/dist/default/lib/canvas-surface.js +25 -0
  27. package/dist/default/lib/clock.js +23 -0
  28. package/dist/default/lib/crossfade.js +1 -0
  29. package/dist/default/lib/envelope-curve.js +1 -0
  30. package/dist/default/lib/peaks-request.js +44 -0
  31. package/dist/default/lib/positions.js +58 -0
  32. package/dist/default/lib/scrub.js +12 -0
  33. package/dist/default/lib/spoken-duration.js +30 -0
  34. package/dist/default/lib/value-model.js +62 -0
  35. package/dist/default/lib/waveform-buckets.js +7 -1
  36. package/dist/default/lib/waveform-scene.js +108 -0
  37. package/dist/default/lib/zoom-gesture.js +97 -1
  38. package/dist/dev/elements/button.d.ts +2 -0
  39. package/dist/dev/elements/button.js +14 -5
  40. package/dist/dev/elements/button.js.map +1 -1
  41. package/dist/dev/elements/dial.d.ts +2 -0
  42. package/dist/dev/elements/dial.js +16 -7
  43. package/dist/dev/elements/dial.js.map +1 -1
  44. package/dist/dev/elements/display-element.d.ts +5 -3
  45. package/dist/dev/elements/display-element.js +11 -9
  46. package/dist/dev/elements/display-element.js.map +1 -1
  47. package/dist/dev/elements/envelope.js +48 -78
  48. package/dist/dev/elements/envelope.js.map +1 -1
  49. package/dist/dev/elements/field-gesture.js +82 -0
  50. package/dist/dev/elements/field-gesture.js.map +1 -0
  51. package/dist/dev/elements/meter.d.ts +10 -5
  52. package/dist/dev/elements/meter.js +5 -1
  53. package/dist/dev/elements/meter.js.map +1 -1
  54. package/dist/dev/elements/number.js +13 -7
  55. package/dist/dev/elements/number.js.map +1 -1
  56. package/dist/dev/elements/position-group.d.ts +12 -6
  57. package/dist/dev/elements/position-group.js +42 -26
  58. package/dist/dev/elements/position-group.js.map +1 -1
  59. package/dist/dev/elements/radio-group.d.ts +11 -7
  60. package/dist/dev/elements/radio-group.js +59 -56
  61. package/dist/dev/elements/radio-group.js.map +1 -1
  62. package/dist/dev/elements/scale.js +3 -3
  63. package/dist/dev/elements/scale.js.map +1 -1
  64. package/dist/dev/elements/segmented.js +12 -5
  65. package/dist/dev/elements/segmented.js.map +1 -1
  66. package/dist/dev/elements/slider.d.ts +4 -2
  67. package/dist/dev/elements/slider.js +25 -20
  68. package/dist/dev/elements/slider.js.map +1 -1
  69. package/dist/dev/elements/sonic-element.d.ts +13 -2
  70. package/dist/dev/elements/sonic-element.js +63 -11
  71. package/dist/dev/elements/sonic-element.js.map +1 -1
  72. package/dist/dev/elements/split.d.ts +1 -0
  73. package/dist/dev/elements/split.js +14 -13
  74. package/dist/dev/elements/split.js.map +1 -1
  75. package/dist/dev/elements/switch.d.ts +2 -2
  76. package/dist/dev/elements/switch.js +38 -47
  77. package/dist/dev/elements/switch.js.map +1 -1
  78. package/dist/dev/elements/toggle.js +32 -49
  79. package/dist/dev/elements/toggle.js.map +1 -1
  80. package/dist/dev/elements/value-element.d.ts +11 -5
  81. package/dist/dev/elements/value-element.js +72 -94
  82. package/dist/dev/elements/value-element.js.map +1 -1
  83. package/dist/dev/elements/value-gestures.js +22 -22
  84. package/dist/dev/elements/value-gestures.js.map +1 -1
  85. package/dist/dev/elements/wave-element.d.ts +8 -5
  86. package/dist/dev/elements/wave-element.js +30 -20
  87. package/dist/dev/elements/wave-element.js.map +1 -1
  88. package/dist/dev/elements/waveform.d.ts +20 -22
  89. package/dist/dev/elements/waveform.js +42 -241
  90. package/dist/dev/elements/waveform.js.map +1 -1
  91. package/dist/dev/elements/wavestrip.d.ts +5 -1
  92. package/dist/dev/elements/wavestrip.js +10 -5
  93. package/dist/dev/elements/wavestrip.js.map +1 -1
  94. package/dist/dev/elements/xy.js +81 -119
  95. package/dist/dev/elements/xy.js.map +1 -1
  96. package/dist/dev/index.d.ts +5 -3
  97. package/dist/dev/index.js +2 -1
  98. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  99. package/dist/dev/lib/canvas-surface.js +25 -0
  100. package/dist/dev/lib/canvas-surface.js.map +1 -1
  101. package/dist/dev/lib/clock.d.ts +5 -0
  102. package/dist/dev/lib/clock.js +24 -0
  103. package/dist/dev/lib/clock.js.map +1 -0
  104. package/dist/dev/lib/crossfade.d.ts +1 -0
  105. package/dist/dev/lib/crossfade.js +1 -0
  106. package/dist/dev/lib/crossfade.js.map +1 -1
  107. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  108. package/dist/dev/lib/envelope-curve.js +1 -0
  109. package/dist/dev/lib/envelope-curve.js.map +1 -1
  110. package/dist/dev/lib/mirror-children.js.map +1 -1
  111. package/dist/dev/lib/owned-control.js.map +1 -1
  112. package/dist/dev/lib/peaks-request.d.ts +5 -0
  113. package/dist/dev/lib/peaks-request.js +45 -0
  114. package/dist/dev/lib/peaks-request.js.map +1 -0
  115. package/dist/dev/lib/positions.d.ts +7 -0
  116. package/dist/dev/lib/positions.js +59 -0
  117. package/dist/dev/lib/positions.js.map +1 -0
  118. package/dist/dev/lib/scrub.d.ts +8 -0
  119. package/dist/dev/lib/scrub.js +13 -0
  120. package/dist/dev/lib/scrub.js.map +1 -0
  121. package/dist/dev/lib/spoken-duration.js +31 -0
  122. package/dist/dev/lib/spoken-duration.js.map +1 -0
  123. package/dist/dev/lib/time-regions.d.ts +1 -0
  124. package/dist/dev/lib/time-regions.js.map +1 -1
  125. package/dist/dev/lib/units.d.ts +8 -0
  126. package/dist/dev/lib/value-model.js +63 -0
  127. package/dist/dev/lib/value-model.js.map +1 -0
  128. package/dist/dev/lib/waveform-buckets.d.ts +2 -1
  129. package/dist/dev/lib/waveform-buckets.js +7 -1
  130. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  131. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  132. package/dist/dev/lib/waveform-scene.js +109 -0
  133. package/dist/dev/lib/waveform-scene.js.map +1 -0
  134. package/dist/dev/lib/zoom-gesture.js +97 -1
  135. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  136. package/dist/skins/amber.css +1 -0
  137. package/dist/skins/ivory.css +1 -0
  138. package/dist/skins/lime.css +1 -0
  139. package/dist/skins/slate.css +1 -0
  140. package/dist/styles/button.css +45 -11
  141. package/dist/styles/dial.css +10 -9
  142. package/dist/styles/envelope.css +10 -4
  143. package/dist/styles/led.css +5 -1
  144. package/dist/styles/material/arc.css +3 -2
  145. package/dist/styles/material/bracket.css +2 -2
  146. package/dist/styles/material/cap.css +33 -12
  147. package/dist/styles/material/core.css +4 -0
  148. package/dist/styles/material/glass.css +1 -1
  149. package/dist/styles/material/groove.css +4 -3
  150. package/dist/styles/material/keycap.css +3 -2
  151. package/dist/styles/material/lens.css +1 -1
  152. package/dist/styles/material/pane.css +5 -4
  153. package/dist/styles/material/readout.css +6 -6
  154. package/dist/styles/material/well.css +2 -2
  155. package/dist/styles/meter.css +2 -7
  156. package/dist/styles/number.css +5 -1
  157. package/dist/styles/panel.css +4 -3
  158. package/dist/styles/ring.css +1 -1
  159. package/dist/styles/screen.css +5 -1
  160. package/dist/styles/segmented.css +17 -4
  161. package/dist/styles/slider.css +9 -3
  162. package/dist/styles/spectrum.css +16 -3
  163. package/dist/styles/switch.css +35 -10
  164. package/dist/styles/toggle.css +14 -7
  165. package/dist/styles/waveform.css +17 -5
  166. package/dist/styles/wavestrip.css +11 -3
  167. package/dist/styles/xy.css +9 -5
  168. package/package.json +1 -6
  169. package/dist/default/dat.js +0 -51
  170. package/dist/default/lib/asked-value.js +0 -21
  171. package/dist/default/lib/fill-watch.js +0 -29
  172. package/dist/dev/dat.d.ts +0 -19
  173. package/dist/dev/dat.js +0 -52
  174. package/dist/dev/dat.js.map +0 -1
  175. package/dist/dev/lib/asked-value.js +0 -22
  176. package/dist/dev/lib/asked-value.js.map +0 -1
  177. package/dist/dev/lib/fill-watch.js +0 -30
  178. 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
@@ -12,11 +12,6 @@ var SonicSegmented = class extends SonicRadioGroupElement {
12
12
  group = renderSegmented();
13
13
  connect(signal) {
14
14
  super.connect(signal);
15
- this.checkStyles(this.group, "segmented.css", [
16
- "well",
17
- "keycap",
18
- ["cap", ".sonic-segmented-option"]
19
- ]);
20
15
  }
21
16
  renderOption() {
22
17
  return renderOption();
@@ -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
  }
@@ -114,13 +118,6 @@ var SonicSlider = class extends SonicValueElement {
114
118
  }
115
119
  });
116
120
  this.render();
117
- this.checkStyles(slider, "slider.css", [
118
- "cap",
119
- "readout",
120
- "value",
121
- "groove",
122
- ["glass", ".sonic-slider-readout"]
123
- ]);
124
121
  this.bindGestures(slider, signal, (event) => {
125
122
  const axis = this.#axis(slider, cap);
126
123
  if (event.target instanceof Node && cap.contains(event.target)) return axis;
@@ -146,11 +143,7 @@ var SonicSlider = class extends SonicValueElement {
146
143
  focusTarget() {
147
144
  return this.#slider;
148
145
  }
149
- heldWrite(next) {
150
- super.heldWrite(next);
151
- if (this.scrub) this.render();
152
- }
153
- holdChanged() {
146
+ scrubChanged() {
154
147
  if (this.scrub) this.render();
155
148
  }
156
149
  scrubsKeyRepeat() {
@@ -199,18 +192,16 @@ var SonicSlider = class extends SonicValueElement {
199
192
  }
200
193
  #renderScrub(slider) {
201
194
  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));
195
+ const state = this.scrubState();
196
+ const { played, scrub } = scrubRegions(state, this.value, this.mapping());
197
+ if (this.scrub && state.from !== void 0) style.setProperty("--_sonic-slider-played", String(played));
207
198
  else style.removeProperty("--_sonic-slider-played");
208
- if (played === scrubbed || !isHeld || !this.isRevealed()) {
199
+ if (scrub === void 0 || !this.scrub) {
209
200
  style.removeProperty("--_sonic-slider-scrub-region");
210
201
  return;
211
202
  }
212
- const from = grooveStop(Math.min(played, scrubbed));
213
- const to = grooveStop(Math.max(played, scrubbed));
203
+ const from = grooveStop(Math.min(played, scrub));
204
+ const to = grooveStop(Math.max(played, scrub));
214
205
  style.setProperty("--_sonic-slider-scrub-region", `linear-gradient(var(--_sonic-slider-toward), transparent ${from}, var(--_sonic-slider-scrub) ${from} ${to}, transparent ${to})`);
215
206
  }
216
207
  };
@@ -58,7 +58,7 @@ var SonicElement = class extends HTMLElement {
58
58
  }
59
59
  if (!this.isDisabled() && !this.#focused()) target.focus(options);
60
60
  }
61
- checkStyles(control, sheet, material = []) {}
61
+ checkStyles(control, sheet, { box, material }) {}
62
62
  focusTarget() {}
63
63
  forwardNaming(target, isNamed) {
64
64
  const labels = isNamed ? this.namingLabels() : [];
@@ -124,6 +124,8 @@ var SonicElement = class extends HTMLElement {
124
124
  this[name] = value;
125
125
  }
126
126
  }
127
+ #checkBox(control, checked, box) {}
128
+ #checkHost(checked) {}
127
129
  #focused() {
128
130
  const root = this.getRootNode();
129
131
  const active = root instanceof ShadowRoot ? root.activeElement : this.ownerDocument.activeElement;
@@ -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