@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,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);
@@ -34,7 +35,6 @@ var SonicSwitch = class extends SonicPositionGroupElement {
34
35
  connect(signal) {
35
36
  const group = this.group;
36
37
  super.connect(signal);
37
- this.checkStyles(group, "switch.css", ["cap"]);
38
38
  this.bare.addEventListener("click", (event) => {
39
39
  const isSwallowed = this.#isClickSwallowed && event instanceof PointerEvent && event.pointerType !== "";
40
40
  this.#isClickSwallowed = false;
@@ -45,20 +45,14 @@ var SonicSwitch = class extends SonicPositionGroupElement {
45
45
  }, { signal });
46
46
  this.#bindBat(group, signal);
47
47
  }
48
- releaseTarget(option) {
49
- const target = this.pressTarget(option);
48
+ releaseTarget(index) {
49
+ const target = this.pressTarget(index);
50
50
  return this.isMomentary(target) || this.isOptionDisabled(target) ? void 0 : target;
51
51
  }
52
52
  render() {
53
53
  if (!this.isBound()) return;
54
54
  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));
55
+ this.#renderThrow(this.checkedIndex());
62
56
  }
63
57
  renderOption() {
64
58
  return renderPosition();
@@ -66,7 +60,7 @@ var SonicSwitch = class extends SonicPositionGroupElement {
66
60
  #bindBat(group, signal) {
67
61
  bindDrag(group, {
68
62
  grab: (event) => {
69
- if (!(event.target === this.bare || this.optionOf(event.target) !== void 0) || this.isDisabled() || !this.#isOnBat(event)) return;
63
+ if (!(this.pressedIndex(event.target) >= 0) || this.isDisabled() || !this.#isOnBat(event)) return;
70
64
  return {
71
65
  hasMoved: false,
72
66
  last: this.along(event),
@@ -87,51 +81,41 @@ var SonicSwitch = class extends SonicPositionGroupElement {
87
81
  }
88
82
  }, signal);
89
83
  }
90
- #hasSprungFrom(option) {
91
- if (!option || !this.isMomentary(option)) return false;
84
+ #hasSprungFrom(index) {
85
+ if (!this.isMomentary(index)) return false;
92
86
  this.releaseHold();
93
- return this.value !== optionValue(option);
87
+ return !this.isChecked(index);
94
88
  }
95
89
  #isOnBat(event) {
96
90
  return isUnder(this.#bat, event.clientX, event.clientY);
97
91
  }
98
92
  #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();
93
+ const from = this.checkedIndex();
94
+ const target = soleStep(this.positions(), from) ?? this.#sideStep(from, event);
95
+ if (target === void 0) return;
96
+ this.chooseByClick(target);
97
+ this.refocusAt(this.checkedIndex());
117
98
  }
118
99
  #renderThrow(index) {
119
- const count = this.options().length;
100
+ const count = this.positions().length;
120
101
  const at = index < 0 || count < 2 ? 0 : 2 * index / (count - 1) - 1;
121
102
  this.group.style.setProperty("--_sonic-switch-at", String(at));
122
103
  }
104
+ #sideStep(from, event) {
105
+ const box = this.#bat.getBoundingClientRect();
106
+ const [start, size] = this.orientation === "horizontal" ? [box.left, box.width] : [box.top, box.height];
107
+ const offset = (this.along(event) - start) / size - .5;
108
+ const side = Math.abs(offset) < pressCenterZone ? 1 : Math.sign(offset);
109
+ const toward = this.stepFrom(from, side);
110
+ return toward !== void 0 && toward !== from ? toward : this.stepFrom(from, -side);
111
+ }
123
112
  #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);
113
+ const from = this.checkedIndex();
114
+ const target = this.stepFrom(from, direction);
115
+ if (target === void 0 || target === from || this.#hasSprungFrom(from)) return;
116
+ if (this.isMomentary(target)) this.hold(target, pointerId);
117
+ else this.select(target);
118
+ this.refocusAt(this.checkedIndex());
135
119
  }
136
120
  };
137
121
 
@@ -1,7 +1,8 @@
1
1
  import { placeChildren, requireChild, template } from "../lib/render.js";
2
2
  import { bindDrag } from "../lib/pointer-drag.js";
3
3
  import { dragThresholdPx } from "../lib/drag-step.js";
4
- import { isUnder, optionValue } from "./radio-group.js";
4
+ import { enabledEnds } from "../lib/positions.js";
5
+ import { isUnder } from "./radio-group.js";
5
6
  import { SonicPositionGroupElement } from "./position-group.js";
6
7
  import { positionAt, travelFrom } from "../lib/toggle-travel.js";
7
8
 
@@ -31,11 +32,6 @@ var SonicToggle = class extends SonicPositionGroupElement {
31
32
  }
32
33
  connect(signal) {
33
34
  super.connect(signal);
34
- this.checkStyles(this.group, "toggle.css", [
35
- "cap",
36
- "well",
37
- "keycap"
38
- ]);
39
35
  this.bare.addEventListener("click", (event) => {
40
36
  if (event instanceof PointerEvent && event.pointerType !== "") return;
41
37
  this.setChecked(!this.checked);
@@ -52,7 +48,7 @@ var SonicToggle = class extends SonicPositionGroupElement {
52
48
  }
53
49
  render() {
54
50
  if (!this.isBound()) return;
55
- this.group.style.setProperty("--_sonic-toggle-count", String(this.#stopCount()));
51
+ this.group.style.setProperty("--_sonic-toggle-count", String(this.positions().length));
56
52
  this.#placeCap(this.#draggedTo ?? this.#restingAt());
57
53
  super.render();
58
54
  for (const [index, option] of this.options().entries()) option.style.setProperty("--_sonic-toggle-index", String(index));
@@ -77,7 +73,7 @@ var SonicToggle = class extends SonicPositionGroupElement {
77
73
  this.#slideTo(positionAt(drag.travel, pointer), event.pointerId);
78
74
  },
79
75
  release: (drag) => {
80
- delete (drag.position ?? this.bare).dataset.sonicPressed;
76
+ delete drag.marked.dataset.sonicPressed;
81
77
  this.#draggedTo = void 0;
82
78
  this.toggleState("dragging", false);
83
79
  this.render();
@@ -85,25 +81,21 @@ var SonicToggle = class extends SonicPositionGroupElement {
85
81
  }, signal);
86
82
  }
87
83
  #checkLegends() {}
88
- #enabledEnds() {
89
- if (this.isBare()) return [0, 1];
90
- const options = this.options();
91
- const isEnabled = (option) => !this.isOptionDisabled(option);
92
- return [options.findIndex(isEnabled), options.findLastIndex(isEnabled)];
93
- }
94
84
  #fixedLegend() {
95
85
  return [...this.children].find((child) => child !== this.group && !super.isMirrored(child));
96
86
  }
97
87
  #grab(event) {
98
- const position = this.optionOf(event.target);
99
- if (this.isDisabled() || !position && event.target !== this.bare) return void 0;
88
+ const position = this.pressedIndex(event.target);
89
+ if (this.isDisabled() || position < 0) return void 0;
100
90
  const isOnCap = isUnder(this.#cap, event.clientX, event.clientY);
101
91
  const travel = this.#travel(event, isOnCap);
102
- if (!travel || this.#isBlocked(position, isOnCap)) return void 0;
103
- (position ?? this.bare).toggleAttribute("data-sonic-pressed", true);
92
+ if (!travel || !isOnCap && this.isOptionDisabled(position)) return void 0;
93
+ const marked = this.options()[position] ?? this.bare;
94
+ marked.toggleAttribute("data-sonic-pressed", true);
104
95
  return {
105
96
  hasMoved: false,
106
97
  isHolding: this.#holdFrom(position, event.pointerId),
98
+ marked,
107
99
  position,
108
100
  start: this.along(event),
109
101
  thresholdPx: dragThresholdPx(event.pointerType),
@@ -111,57 +103,36 @@ var SonicToggle = class extends SonicPositionGroupElement {
111
103
  };
112
104
  }
113
105
  #holdFrom(position, pointerId) {
114
- const target = position && this.pressTarget(position);
115
- if (!target || !this.isMomentary(target) || this.isOptionDisabled(target)) return false;
106
+ const target = this.pressTarget(position);
107
+ if (!this.isMomentary(target) || this.isOptionDisabled(target)) return false;
116
108
  this.hold(target, pointerId);
117
109
  return true;
118
110
  }
119
- #isBlocked(position, isOnCap) {
120
- return !isOnCap && position !== void 0 && this.isOptionDisabled(position);
121
- }
122
- #onAt() {
123
- return this.orientation === "vertical" ? 0 : 1;
124
- }
125
111
  #placeCap(at) {
126
112
  this.group.style.setProperty("--_sonic-toggle-at", String(at));
127
113
  }
128
114
  #press(position) {
129
- if (!position) {
130
- this.setChecked(!this.checked);
131
- return;
132
- }
133
115
  const target = this.pressTarget(position);
134
116
  if (this.isOptionDisabled(target)) return;
135
117
  this.select(target);
136
- this.#refocus(target);
137
- }
138
- #refocus(position) {
139
- if (this.group.matches(":focus-within")) position.focus();
118
+ this.refocusAt(target);
140
119
  }
141
120
  #restingAt() {
142
- if (this.isBare()) return this.checked ? this.#onAt() : 1 - this.#onAt();
143
- return Math.max(0, this.checkedIndex(this.options()));
121
+ return Math.max(0, this.checkedIndex());
144
122
  }
145
123
  #slideTo(at, pointerId) {
146
- const crossed = this.options()[Math.round(at)];
124
+ const crossed = Math.round(at);
147
125
  this.#draggedTo = at;
148
126
  this.#placeCap(at);
149
- if (this.isBare()) {
150
- this.setChecked(Math.round(at) === this.#onAt());
151
- return;
152
- }
153
- if (!crossed || optionValue(crossed) === this.value) return;
127
+ if (this.isChecked(crossed)) return;
154
128
  this.releaseHold();
155
- if (this.isOptionDisabled(crossed) || optionValue(crossed) === this.value) return;
129
+ if (this.isOptionDisabled(crossed) || this.isChecked(crossed)) return;
156
130
  if (this.isMomentary(crossed)) this.hold(crossed, pointerId);
157
131
  else this.select(crossed);
158
- this.#refocus(crossed);
159
- }
160
- #stopCount() {
161
- return this.options().length || 2;
132
+ this.refocusAt(crossed);
162
133
  }
163
134
  #travel(event, isOnCap) {
164
- const [first, last] = this.#enabledEnds();
135
+ const [first, last] = enabledEnds(this.positions());
165
136
  if (first < 0) return void 0;
166
137
  const box = this.#well.getBoundingClientRect();
167
138
  const [start, length, thickness] = this.orientation === "vertical" ? [
@@ -175,7 +146,7 @@ var SonicToggle = class extends SonicPositionGroupElement {
175
146
  ];
176
147
  return travelFrom({
177
148
  at: this.#restingAt(),
178
- count: this.#stopCount(),
149
+ count: this.positions().length,
179
150
  first,
180
151
  isOnCap,
181
152
  last,
@@ -1,13 +1,12 @@
1
- import { clamp, toNumber, trimFloat } from "../lib/math.js";
1
+ import { toNumber, trimFloat } from "../lib/math.js";
2
2
  import { writeAttribute } from "../lib/write-attribute.js";
3
3
  import { SonicFormElement } from "./form-element.js";
4
4
  import { Readout } from "./readout.js";
5
5
  import { readPxProperty } from "../lib/read-px-property.js";
6
6
  import { parseNumberList } from "../lib/number-list.js";
7
- import { valueMapping } from "../lib/value-mapping.js";
8
7
  import { ValueGestures } from "./value-gestures.js";
9
- import { moveTo, resnap, setAsked } from "../lib/asked-value.js";
10
8
  import { bindHoverPreview } from "../lib/hover-preview.js";
9
+ import { createValueModel } from "../lib/value-model.js";
11
10
 
12
11
  //#region src/elements/value-element.ts
13
12
  const fallbackTravelPx = 160;
@@ -56,20 +55,7 @@ var SonicValueElement = class extends SonicFormElement {
56
55
  input: (next, isMover = false) => (isMover || !element.#isHeld()) && element.input(next),
57
56
  isDisabled: () => element.isDisabled(),
58
57
  isHeld: () => element.#isHeld(),
59
- land: (resolve) => {
60
- element.#land = resolve;
61
- },
62
- limit: (bounds) => {
63
- element.#limit = bounds;
64
- },
65
- mapping: () => element.mapping(),
66
- value: () => element.#cell.value,
67
- watch: (listener) => {
68
- element.#watchers.add(listener);
69
- return () => {
70
- element.#watchers.delete(listener);
71
- };
72
- }
58
+ model: element.#model
73
59
  });
74
60
  }
75
61
  get cancelling() {
@@ -177,6 +163,7 @@ var SonicValueElement = class extends SonicFormElement {
177
163
  get pointerType() {
178
164
  return this.#gestures?.pointerType();
179
165
  }
166
+ /** The values a stepped control stops at, spaced evenly along the travel whatever their spacing */
180
167
  get positions() {
181
168
  return parseNumberList(this.getAttribute("positions"));
182
169
  }
@@ -192,6 +179,7 @@ var SonicValueElement = class extends SonicFormElement {
192
179
  get revealed() {
193
180
  return this.hasState("revealed");
194
181
  }
182
+ /** Rounds the value assistive technology hears, in value units; the value itself is untouched */
195
183
  get spokenStep() {
196
184
  return this.optionalNumberAttribute("spoken-step");
197
185
  }
@@ -211,7 +199,7 @@ var SonicValueElement = class extends SonicFormElement {
211
199
  this.reflect("taper", curve);
212
200
  }
213
201
  get value() {
214
- return this.#cell.value;
202
+ return this.#model.value;
215
203
  }
216
204
  set value(next) {
217
205
  if (next === void 0 || next === null) {
@@ -220,17 +208,17 @@ var SonicValueElement = class extends SonicFormElement {
220
208
  }
221
209
  const value = toNumber(next);
222
210
  if (this.#isHeld()) {
223
- this.heldWrite(value);
211
+ this.#writeHeld(value);
224
212
  return;
225
213
  }
226
- if (setAsked(this.#cell, this.mapping(), value)) this.render();
214
+ if (this.#model.write(value)) this.render();
227
215
  }
228
216
  get valueText() {
229
- return this.#textFor(this.#cell.value);
217
+ return this.#textFor(this.#model.value);
230
218
  }
231
- #cell = {
232
- asked: void 0,
233
- value: 0
219
+ notation = {
220
+ format: String,
221
+ parse: Number.parseFloat
234
222
  };
235
223
  #formatEntry;
236
224
  #formatSpokenValue;
@@ -239,25 +227,16 @@ var SonicValueElement = class extends SonicFormElement {
239
227
  #heldAt;
240
228
  #hover;
241
229
  #instance = String(instanceCount += 1);
242
- #land;
243
- #limit;
244
- #mapping;
230
+ #model = createValueModel(() => this.#spec());
245
231
  #parseValue;
246
- #positions;
247
232
  #readout;
248
- #watchers = /* @__PURE__ */ new Set();
249
233
  attributeChangedCallback(name) {
250
- this.#mapping = void 0;
251
- this.#positions = parseNumberList(this.getAttribute("positions"));
252
- if (name === "value") {
253
- if (this.#isHeld()) {
254
- this.heldWrite(this.numberAttribute("value", this.min));
255
- return;
256
- }
257
- this.#cell.asked = void 0;
234
+ if (name === "value" && this.#isHeld()) {
235
+ this.#writeHeld(this.numberAttribute("value", this.min));
236
+ return;
258
237
  }
259
238
  if (name === "disabled" && this.isDisabled()) this.#gestures?.end();
260
- resnap(this.#cell, this.mapping(), this.numberAttribute("value", this.min));
239
+ this.#model.respec(this.numberAttribute("value", this.min), { forgetAsk: name === "value" });
261
240
  if (proportionAttributes.has(name) && this.isBound()) this.proportionsChanged();
262
241
  this.render();
263
242
  }
@@ -287,26 +266,23 @@ var SonicValueElement = class extends SonicFormElement {
287
266
  this.forwardNaming(target, isNamed);
288
267
  },
289
268
  hasEntry: () => this.entry !== "none",
290
- holdChanged: () => {
291
- this.#renderHold();
292
- },
293
269
  input: (next) => this.input(next),
294
270
  isDisabled: () => this.isDisabled(),
295
271
  keyStep: () => this.keyStep,
296
- land: (target, direction) => this.#land?.(target, direction) ?? target,
297
- limit: () => this.#limit,
298
- mapping: () => this.mapping(),
299
- parse: (text) => (this.#parseValue ?? Number.parseFloat)(text),
272
+ model: this.#model,
273
+ parse: (text) => (this.#parseValue ?? this.notation.parse)(text),
300
274
  render: () => {
301
275
  this.render();
302
276
  },
277
+ scrubChanged: () => {
278
+ this.#renderHold();
279
+ },
303
280
  scrubsKeyRepeat: () => this.scrubsKeyRepeat(),
304
281
  springTarget: () => this.springTarget(),
305
282
  tapTarget: () => this.tapTarget(),
306
283
  toggleState: (state, isOn) => {
307
284
  this.toggleState(state, isOn);
308
- },
309
- value: () => this.#cell.value
285
+ }
310
286
  }, control);
311
287
  this.#gestures.bind(signal, grab);
312
288
  control.addEventListener("pointerup", (event) => {
@@ -322,7 +298,7 @@ var SonicValueElement = class extends SonicFormElement {
322
298
  dismiss: () => {
323
299
  this.render();
324
300
  },
325
- isTaken: () => this.#isEditing() || this.isRevealed(),
301
+ isTaken: () => this.#isEditing() || this.#isRevealed(),
326
302
  show: () => {
327
303
  if (!this.readout) return;
328
304
  this.renderReadout();
@@ -335,60 +311,32 @@ var SonicValueElement = class extends SonicFormElement {
335
311
  controlRole() {
336
312
  return "slider";
337
313
  }
338
- heldFrom() {
339
- return this.#gestures?.heldFrom();
340
- }
341
- heldWrite(next) {
342
- this.#heldAt = next;
343
- }
344
- holdChanged() {}
345
314
  input(next) {
346
- const previous = this.#cell.value;
347
- const target = this.#limit ? clamp(next, ...this.#limit) : next;
348
- if (moveTo(this.#cell, this.mapping(), target)) this.render();
349
- if (this.#cell.value === previous) return false;
315
+ const previous = this.#model.value;
316
+ if (this.#model.input(next)) this.render();
317
+ if (this.#model.value === previous) return false;
350
318
  this.dispatchEvent(new Event("input", {
351
319
  bubbles: true,
352
320
  composed: true
353
321
  }));
354
322
  return true;
355
323
  }
356
- isRevealed() {
357
- return this.#gestures?.isRevealed() === true;
358
- }
359
324
  isWrapping() {
360
325
  return false;
361
326
  }
362
327
  mapping() {
363
- if (this.#mapping) return this.#mapping;
364
- const { detent, midpoint } = this;
365
- this.#mapping = valueMapping({
366
- ...detent === void 0 ? {} : { detent },
367
- ...this.#positions ? { positions: this.#positions } : {},
368
- max: this.max,
369
- ...midpoint === void 0 ? {} : { midpoint },
370
- isNotched: this.notched,
371
- isWrapping: this.isWrapping(),
372
- min: this.min,
373
- step: this.step,
374
- taper: this.taper
375
- });
376
- return this.#mapping;
328
+ return this.#model.mapping();
377
329
  }
378
330
  originValue() {
379
331
  return this.origin ?? this.mapping().bounds[0];
380
332
  }
381
- playback() {
382
- const from = this.heldFrom();
383
- return from === void 0 ? this.#cell.value : this.#heldAt ?? from;
384
- }
385
333
  proportionsChanged() {}
386
334
  readoutValue() {
387
- return this.#shown().value ?? this.#cell.value;
335
+ return this.#shown().value ?? this.#model.value;
388
336
  }
389
337
  render() {
390
338
  if (this.isBound()) this.#renderControl();
391
- for (const listener of this.#watchers) listener();
339
+ this.#model.notify();
392
340
  }
393
341
  renderReadout() {
394
342
  const readout = this.#readout;
@@ -404,18 +352,27 @@ var SonicValueElement = class extends SonicFormElement {
404
352
  restoreState(state) {
405
353
  this.value = Number(state);
406
354
  }
355
+ scrubChanged() {}
407
356
  scrubsKeyRepeat() {
408
357
  return false;
409
358
  }
359
+ scrubState() {
360
+ const from = this.#gestures?.scrubFrom();
361
+ return {
362
+ from,
363
+ isRevealed: this.#isRevealed(),
364
+ played: from === void 0 ? this.#model.value : this.#heldAt ?? from
365
+ };
366
+ }
410
367
  springTarget() {}
411
368
  tapTarget() {}
412
369
  travelPx(control, property) {
413
370
  return Math.max(1, readPxProperty(getComputedStyle(control), property, fallbackTravelPx));
414
371
  }
415
372
  #entryText() {
416
- const value = this.#cell.value;
373
+ const value = this.#model.value;
417
374
  if (this.#formatEntry) return this.#formatEntry(value);
418
- if (!this.#parseValue) return String(value);
375
+ if (!this.#parseValue) return this.notation.format(value);
419
376
  return this.valueText;
420
377
  }
421
378
  #isEditing() {
@@ -424,8 +381,11 @@ var SonicValueElement = class extends SonicFormElement {
424
381
  #isHeld() {
425
382
  return this.#gestures?.isHeld() === true;
426
383
  }
384
+ #isRevealed() {
385
+ return this.#gestures?.isRevealed() === true;
386
+ }
427
387
  #renderAria(control, orientation) {
428
- this.writeFormValue(String(this.#cell.value), String(this.#cell.value));
388
+ this.writeFormValue(String(this.#model.value), String(this.#model.value));
429
389
  if (this.#isEditing()) {
430
390
  for (const name of roleAttributes) control.removeAttribute(name);
431
391
  this.forwardNaming(control, false);
@@ -438,15 +398,15 @@ var SonicValueElement = class extends SonicFormElement {
438
398
  writeAttribute(control, "aria-valuemax", String(high));
439
399
  writeAttribute(control, "aria-valuenow", String(this.#spokenNow()));
440
400
  if (orientation) writeAttribute(control, "aria-orientation", orientation);
441
- writeAttribute(control, "aria-valuetext", (this.#formatSpokenValue ?? this.#formatValue)?.(this.#spokenNow()));
401
+ writeAttribute(control, "aria-valuetext", (this.#formatSpokenValue ?? this.#formatValue ?? this.notation.speak)?.(this.#spokenNow()));
442
402
  this.forwardNaming(control, true);
443
403
  this.#renderDisabled(control);
444
404
  this.renderReadout();
445
405
  }
446
406
  #renderControl() {
447
- if (this.#isEditing() || this.isRevealed()) this.#hover?.clear();
407
+ if (this.#isEditing() || this.#isRevealed()) this.#hover?.clear();
448
408
  this.draw();
449
- this.toggleState("at-origin", !this.isWrapping() && this.#cell.value === this.mapping().snap(this.originValue()));
409
+ this.toggleState("at-origin", !this.isWrapping() && this.#model.value === this.mapping().snap(this.originValue()));
450
410
  this.#renderAria(this.focusTarget(), this.controlOrientation());
451
411
  }
452
412
  #renderDisabled(control) {
@@ -456,29 +416,47 @@ var SonicValueElement = class extends SonicFormElement {
456
416
  writeAttribute(control, "tabindex", isDisabled ? void 0 : stop);
457
417
  }
458
418
  #renderHold() {
459
- if (this.heldFrom() === void 0) this.#heldAt = void 0;
419
+ if (this.#gestures?.scrubFrom() === void 0) this.#heldAt = void 0;
460
420
  this.#hover?.restore();
461
421
  this.renderReadout();
462
- this.holdChanged();
422
+ this.scrubChanged();
463
423
  }
464
424
  #shown() {
465
425
  if (this.#isEditing()) return {
466
426
  isOpen: true,
467
427
  value: void 0
468
428
  };
469
- const isRevealed = this.isRevealed();
429
+ const isRevealed = this.#isRevealed();
470
430
  const preview = isRevealed ? void 0 : this.#hover?.value();
471
431
  return {
472
432
  isOpen: (isRevealed || preview !== void 0) && this.readout,
473
- value: preview ?? this.#cell.value
433
+ value: preview ?? this.#model.value
434
+ };
435
+ }
436
+ #spec() {
437
+ const { detent, midpoint, positions } = this;
438
+ return {
439
+ ...detent === void 0 ? {} : { detent },
440
+ ...positions ? { positions } : {},
441
+ max: this.max,
442
+ ...midpoint === void 0 ? {} : { midpoint },
443
+ isNotched: this.notched,
444
+ isWrapping: this.isWrapping(),
445
+ min: this.min,
446
+ step: this.step,
447
+ taper: this.taper
474
448
  };
475
449
  }
476
450
  #spokenNow() {
477
451
  const step = this.spokenStep;
478
- return step !== void 0 && step > 0 ? trimFloat(Math.round(this.#cell.value / step) * step) : this.#cell.value;
452
+ return step !== void 0 && step > 0 ? trimFloat(Math.round(this.#model.value / step) * step) : this.#model.value;
479
453
  }
480
454
  #textFor(value) {
481
- return this.#formatValue?.(value) ?? String(value);
455
+ return (this.#formatValue ?? this.notation.format)(value);
456
+ }
457
+ #writeHeld(next) {
458
+ this.#heldAt = next;
459
+ this.scrubChanged();
482
460
  }
483
461
  };
484
462