@xsynaptic/sonic-ui 0.4.0 → 0.6.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 (247) hide show
  1. package/README.md +3 -20
  2. package/dist/default/dat.js +51 -0
  3. package/dist/default/define/button.js +7 -0
  4. package/dist/default/define/envelope.js +7 -0
  5. package/dist/default/define/split.js +7 -0
  6. package/dist/default/define/switch.js +7 -0
  7. package/dist/default/define/waveform.js +7 -0
  8. package/dist/default/define/wavestrip.js +7 -0
  9. package/dist/default/define/xy.js +7 -0
  10. package/dist/default/define.js +7 -1
  11. package/dist/default/elements/button.js +165 -0
  12. package/dist/default/elements/dial.js +16 -22
  13. package/dist/default/elements/envelope.js +383 -0
  14. package/dist/default/elements/meter.js +155 -63
  15. package/dist/default/elements/number.js +22 -9
  16. package/dist/default/elements/radio-group.js +183 -0
  17. package/dist/default/elements/readout-claim.js +81 -0
  18. package/dist/default/elements/readout.js +35 -0
  19. package/dist/default/elements/segmented.js +9 -159
  20. package/dist/default/elements/slider.js +42 -51
  21. package/dist/default/elements/sonic-element.js +69 -31
  22. package/dist/default/elements/split.js +249 -0
  23. package/dist/default/elements/switch.js +273 -0
  24. package/dist/default/elements/value-element.js +694 -0
  25. package/dist/default/elements/{range-entry.js → value-entry.js} +3 -3
  26. package/dist/default/elements/wave-element.js +80 -0
  27. package/dist/default/elements/waveform.js +328 -0
  28. package/dist/default/elements/wavestrip.js +185 -0
  29. package/dist/default/elements/xy.js +487 -0
  30. package/dist/default/index.js +10 -2
  31. package/dist/default/lib/adsr-shape.js +113 -0
  32. package/dist/default/lib/ballistics.js +55 -0
  33. package/dist/default/lib/canvas-surface.js +203 -0
  34. package/dist/default/lib/copy-node.js +32 -2
  35. package/dist/default/lib/crossfade.js +22 -0
  36. package/dist/default/lib/distribute.js +215 -0
  37. package/dist/default/lib/drag-step.js +99 -0
  38. package/dist/default/lib/envelope-curve.js +9 -0
  39. package/dist/default/lib/field.js +94 -0
  40. package/dist/default/lib/focus-by-pointer.js +28 -0
  41. package/dist/default/lib/frame-timeline.js +63 -0
  42. package/dist/default/lib/hold.js +38 -0
  43. package/dist/default/lib/marker-band.js +80 -0
  44. package/dist/default/lib/marker-rider.js +120 -0
  45. package/dist/default/lib/math.js +17 -3
  46. package/dist/default/lib/modifier-press.js +11 -0
  47. package/dist/default/lib/number-list.js +6 -6
  48. package/dist/default/lib/pointer-drag.js +46 -0
  49. package/dist/default/lib/render.js +20 -5
  50. package/dist/default/lib/resample-peaks.js +46 -0
  51. package/dist/default/lib/strip-scene.js +48 -0
  52. package/dist/default/lib/taper.js +23 -17
  53. package/dist/default/lib/time-regions.js +8 -0
  54. package/dist/default/lib/tracking-clock.js +35 -0
  55. package/dist/default/lib/value-mapping.js +115 -0
  56. package/dist/default/lib/waveform-buckets.js +41 -0
  57. package/dist/default/lib/write-attribute.js +1 -0
  58. package/dist/dev/dat.d.ts +19 -0
  59. package/dist/dev/dat.js +52 -0
  60. package/dist/dev/dat.js.map +1 -0
  61. package/dist/dev/define/button.d.ts +2 -0
  62. package/dist/dev/define/button.js +8 -0
  63. package/dist/dev/define/button.js.map +1 -0
  64. package/dist/dev/define/dial.js.map +1 -1
  65. package/dist/dev/define/envelope.d.ts +2 -0
  66. package/dist/dev/define/envelope.js +8 -0
  67. package/dist/dev/define/envelope.js.map +1 -0
  68. package/dist/dev/define/meter.js.map +1 -1
  69. package/dist/dev/define/number.js.map +1 -1
  70. package/dist/dev/define/segmented.js.map +1 -1
  71. package/dist/dev/define/slider.js.map +1 -1
  72. package/dist/dev/define/split.d.ts +2 -0
  73. package/dist/dev/define/split.js +8 -0
  74. package/dist/dev/define/split.js.map +1 -0
  75. package/dist/dev/define/switch.d.ts +2 -0
  76. package/dist/dev/define/switch.js +8 -0
  77. package/dist/dev/define/switch.js.map +1 -0
  78. package/dist/dev/define/waveform.d.ts +2 -0
  79. package/dist/dev/define/waveform.js +8 -0
  80. package/dist/dev/define/waveform.js.map +1 -0
  81. package/dist/dev/define/wavestrip.d.ts +2 -0
  82. package/dist/dev/define/wavestrip.js +8 -0
  83. package/dist/dev/define/wavestrip.js.map +1 -0
  84. package/dist/dev/define/xy.d.ts +2 -0
  85. package/dist/dev/define/xy.js +8 -0
  86. package/dist/dev/define/xy.js.map +1 -0
  87. package/dist/dev/define.d.ts +15 -3
  88. package/dist/dev/define.js +7 -1
  89. package/dist/dev/elements/{key.d.ts → button.d.ts} +8 -6
  90. package/dist/dev/elements/button.js +166 -0
  91. package/dist/dev/elements/button.js.map +1 -0
  92. package/dist/dev/elements/dial.d.ts +6 -4
  93. package/dist/dev/elements/dial.js +16 -22
  94. package/dist/dev/elements/dial.js.map +1 -1
  95. package/dist/dev/elements/envelope.d.ts +36 -0
  96. package/dist/dev/elements/envelope.js +388 -0
  97. package/dist/dev/elements/envelope.js.map +1 -0
  98. package/dist/dev/elements/form-element.d.ts +1 -1
  99. package/dist/dev/elements/form-element.js.map +1 -1
  100. package/dist/dev/elements/meter.d.ts +11 -6
  101. package/dist/dev/elements/meter.js +155 -63
  102. package/dist/dev/elements/meter.js.map +1 -1
  103. package/dist/dev/elements/number.d.ts +7 -3
  104. package/dist/dev/elements/number.js +22 -9
  105. package/dist/dev/elements/number.js.map +1 -1
  106. package/dist/dev/elements/radio-group.d.ts +25 -0
  107. package/dist/dev/elements/radio-group.js +184 -0
  108. package/dist/dev/elements/radio-group.js.map +1 -0
  109. package/dist/dev/elements/readout-claim.js +82 -0
  110. package/dist/dev/elements/readout-claim.js.map +1 -0
  111. package/dist/dev/elements/readout.js +36 -0
  112. package/dist/dev/elements/readout.js.map +1 -0
  113. package/dist/dev/elements/segmented.d.ts +4 -10
  114. package/dist/dev/elements/segmented.js +9 -159
  115. package/dist/dev/elements/segmented.js.map +1 -1
  116. package/dist/dev/elements/slider.d.ts +6 -6
  117. package/dist/dev/elements/slider.js +42 -51
  118. package/dist/dev/elements/slider.js.map +1 -1
  119. package/dist/dev/elements/sonic-element.d.ts +6 -3
  120. package/dist/dev/elements/sonic-element.js +119 -40
  121. package/dist/dev/elements/sonic-element.js.map +1 -1
  122. package/dist/dev/elements/split.d.ts +21 -0
  123. package/dist/dev/elements/split.js +258 -0
  124. package/dist/dev/elements/split.js.map +1 -0
  125. package/dist/dev/elements/switch.d.ts +35 -0
  126. package/dist/dev/elements/switch.js +274 -0
  127. package/dist/dev/elements/switch.js.map +1 -0
  128. package/dist/dev/elements/value-element.d.ts +84 -0
  129. package/dist/dev/elements/value-element.js +695 -0
  130. package/dist/dev/elements/value-element.js.map +1 -0
  131. package/dist/dev/elements/{range-entry.js → value-entry.js} +4 -4
  132. package/dist/dev/elements/value-entry.js.map +1 -0
  133. package/dist/dev/elements/wave-element.d.ts +37 -0
  134. package/dist/dev/elements/wave-element.js +81 -0
  135. package/dist/dev/elements/wave-element.js.map +1 -0
  136. package/dist/dev/elements/waveform.d.ts +55 -0
  137. package/dist/dev/elements/waveform.js +329 -0
  138. package/dist/dev/elements/waveform.js.map +1 -0
  139. package/dist/dev/elements/wavestrip.d.ts +51 -0
  140. package/dist/dev/elements/wavestrip.js +186 -0
  141. package/dist/dev/elements/wavestrip.js.map +1 -0
  142. package/dist/dev/elements/xy.d.ts +62 -0
  143. package/dist/dev/elements/xy.js +488 -0
  144. package/dist/dev/elements/xy.js.map +1 -0
  145. package/dist/dev/index.d.ts +11 -2
  146. package/dist/dev/index.js +10 -2
  147. package/dist/dev/lib/adsr-shape.js +114 -0
  148. package/dist/dev/lib/adsr-shape.js.map +1 -0
  149. package/dist/dev/lib/ballistics.js +56 -0
  150. package/dist/dev/lib/ballistics.js.map +1 -0
  151. package/dist/dev/lib/canvas-surface.d.ts +26 -0
  152. package/dist/dev/lib/canvas-surface.js +204 -0
  153. package/dist/dev/lib/canvas-surface.js.map +1 -0
  154. package/dist/dev/lib/copy-node.js +32 -2
  155. package/dist/dev/lib/copy-node.js.map +1 -1
  156. package/dist/dev/lib/crossfade.d.ts +12 -0
  157. package/dist/dev/lib/crossfade.js +23 -0
  158. package/dist/dev/lib/crossfade.js.map +1 -0
  159. package/dist/dev/lib/distribute.d.ts +4 -0
  160. package/dist/dev/lib/distribute.js +216 -0
  161. package/dist/dev/lib/distribute.js.map +1 -0
  162. package/dist/dev/lib/drag-step.js +100 -0
  163. package/dist/dev/lib/drag-step.js.map +1 -0
  164. package/dist/dev/lib/envelope-curve.d.ts +4 -0
  165. package/dist/dev/lib/envelope-curve.js +10 -0
  166. package/dist/dev/lib/envelope-curve.js.map +1 -0
  167. package/dist/dev/lib/field.d.ts +4 -0
  168. package/dist/dev/lib/field.js +95 -0
  169. package/dist/dev/lib/field.js.map +1 -0
  170. package/dist/dev/lib/focus-by-pointer.js +29 -0
  171. package/dist/dev/lib/focus-by-pointer.js.map +1 -0
  172. package/dist/dev/lib/frame-timeline.js +64 -0
  173. package/dist/dev/lib/frame-timeline.js.map +1 -0
  174. package/dist/dev/lib/hold.js +39 -0
  175. package/dist/dev/lib/hold.js.map +1 -0
  176. package/dist/dev/lib/marker-band.js +81 -0
  177. package/dist/dev/lib/marker-band.js.map +1 -0
  178. package/dist/dev/lib/marker-rider.js +121 -0
  179. package/dist/dev/lib/marker-rider.js.map +1 -0
  180. package/dist/dev/lib/math.js +17 -3
  181. package/dist/dev/lib/math.js.map +1 -1
  182. package/dist/dev/lib/modifier-press.js +12 -0
  183. package/dist/dev/lib/modifier-press.js.map +1 -0
  184. package/dist/dev/lib/number-list.js +6 -6
  185. package/dist/dev/lib/number-list.js.map +1 -1
  186. package/dist/dev/lib/pointer-drag.js +47 -0
  187. package/dist/dev/lib/pointer-drag.js.map +1 -0
  188. package/dist/dev/lib/read-px-property.js.map +1 -1
  189. package/dist/dev/lib/render.js +20 -5
  190. package/dist/dev/lib/render.js.map +1 -1
  191. package/dist/dev/lib/resample-peaks.js +47 -0
  192. package/dist/dev/lib/resample-peaks.js.map +1 -0
  193. package/dist/dev/lib/strip-scene.js +49 -0
  194. package/dist/dev/lib/strip-scene.js.map +1 -0
  195. package/dist/dev/lib/taper.js +23 -17
  196. package/dist/dev/lib/taper.js.map +1 -1
  197. package/dist/dev/lib/time-regions.d.ts +4 -0
  198. package/dist/dev/lib/time-regions.js +9 -0
  199. package/dist/dev/lib/time-regions.js.map +1 -0
  200. package/dist/dev/lib/tracking-clock.js +36 -0
  201. package/dist/dev/lib/tracking-clock.js.map +1 -0
  202. package/dist/dev/lib/value-mapping.d.ts +23 -0
  203. package/dist/dev/lib/value-mapping.js +116 -0
  204. package/dist/dev/lib/value-mapping.js.map +1 -0
  205. package/dist/dev/lib/waveform-buckets.d.ts +9 -0
  206. package/dist/dev/lib/waveform-buckets.js +42 -0
  207. package/dist/dev/lib/waveform-buckets.js.map +1 -0
  208. package/dist/dev/lib/write-attribute.js +1 -0
  209. package/dist/dev/lib/write-attribute.js.map +1 -1
  210. package/dist/skins/amber.css +8 -0
  211. package/dist/skins/flat.css +25 -0
  212. package/dist/skins/ivory.css +2 -2
  213. package/dist/skins/lime.css +2 -0
  214. package/dist/skins/slate.css +2 -0
  215. package/dist/styles/button.css +240 -0
  216. package/dist/styles/controls.css +8 -1
  217. package/dist/styles/dial.css +110 -56
  218. package/dist/styles/envelope.css +204 -0
  219. package/dist/styles/led.css +39 -8
  220. package/dist/styles/material.css +343 -100
  221. package/dist/styles/meter.css +41 -52
  222. package/dist/styles/number.css +5 -1
  223. package/dist/styles/panel.css +61 -0
  224. package/dist/styles/ring.css +74 -0
  225. package/dist/styles/segmented.css +46 -51
  226. package/dist/styles/slider.css +177 -40
  227. package/dist/styles/switch.css +386 -0
  228. package/dist/styles/waveform.css +196 -0
  229. package/dist/styles/wavestrip.css +232 -0
  230. package/dist/styles/xy.css +241 -0
  231. package/package.json +15 -6
  232. package/dist/default/define/key.js +0 -7
  233. package/dist/default/elements/key.js +0 -169
  234. package/dist/default/elements/range-element.js +0 -546
  235. package/dist/default/lib/detent.js +0 -17
  236. package/dist/dev/define/key.d.ts +0 -2
  237. package/dist/dev/define/key.js +0 -8
  238. package/dist/dev/define/key.js.map +0 -1
  239. package/dist/dev/elements/key.js +0 -170
  240. package/dist/dev/elements/key.js.map +0 -1
  241. package/dist/dev/elements/range-element.d.ts +0 -64
  242. package/dist/dev/elements/range-element.js +0 -547
  243. package/dist/dev/elements/range-element.js.map +0 -1
  244. package/dist/dev/elements/range-entry.js.map +0 -1
  245. package/dist/dev/lib/detent.js +0 -18
  246. package/dist/dev/lib/detent.js.map +0 -1
  247. package/dist/styles/key.css +0 -242
@@ -1,20 +1,16 @@
1
1
  import { requireChild, template } from "../lib/render.js";
2
- import { SonicRangeElement } from "./range-element.js";
2
+ import { SonicValueElement } from "./value-element.js";
3
3
 
4
4
  //#region src/elements/slider.ts
5
- function readRanges(ranges) {
6
- if ("start" in ranges) return Array.from({ length: ranges.length }, (_entry, index) => [ranges.start(index), ranges.end(index)]);
7
- return [...ranges].map(([start, end]) => [start, end]);
8
- }
9
- function grooveStop(fraction) {
10
- if (fraction <= 0) return "0%";
11
- if (fraction >= 1) return "100%";
12
- return `calc(${String(fraction)} * var(--_sonic-slider-travel) + var(--_sonic-slider-groove) / 2)`;
5
+ function grooveStop(proportion) {
6
+ if (proportion <= 0) return "0%";
7
+ if (proportion >= 1) return "100%";
8
+ return `calc(${String(proportion)} * var(--_sonic-slider-travel) + var(--_sonic-slider-groove) / 2)`;
13
9
  }
14
10
  const renderSlider = template(`
15
11
  <div class="sonic-slider" role="slider" tabindex="0">
16
12
  <div class="sonic-slider-scale" aria-hidden="true"></div>
17
- <div class="sonic-slider-marks"></div>
13
+ <div class="sonic-slider-notches"></div>
18
14
  <div class="sonic-slider-groove"></div>
19
15
  <div class="sonic-slider-modulation"></div>
20
16
  <div class="sonic-slider-cap"></div>
@@ -31,20 +27,13 @@ const renderSlider = template(`
31
27
  </div>
32
28
  </div>
33
29
  `, HTMLDivElement);
34
- var SonicSlider = class extends SonicRangeElement {
30
+ var SonicSlider = class extends SonicValueElement {
35
31
  static observedAttributes = [
36
- ...SonicRangeElement.observedAttributes,
32
+ ...SonicValueElement.observedAttributes,
37
33
  "modulation",
38
34
  "orientation",
39
35
  "spring"
40
36
  ];
41
- get buffered() {
42
- return this.#buffered.map(([start, end]) => [start, end]);
43
- }
44
- set buffered(ranges) {
45
- this.#buffered = ranges ? readRanges(ranges).toSorted((first, second) => first[0] - second[0]) : [];
46
- this.render();
47
- }
48
37
  get groovePress() {
49
38
  return this.getAttribute("groove-press") === "none" ? "none" : "jump";
50
39
  }
@@ -57,6 +46,12 @@ var SonicSlider = class extends SonicRangeElement {
57
46
  set modulation(amount) {
58
47
  this.reflect("modulation", amount);
59
48
  }
49
+ get modulationValue() {
50
+ return this.readModulationValue();
51
+ }
52
+ set modulationValue(value) {
53
+ this.writeModulationValue(this.#slider, "slider", value);
54
+ }
60
55
  get orientation() {
61
56
  return this.getAttribute("orientation") === "vertical" ? "vertical" : "horizontal";
62
57
  }
@@ -69,75 +64,71 @@ var SonicSlider = class extends SonicRangeElement {
69
64
  set spring(isSpring) {
70
65
  this.reflect("spring", isSpring);
71
66
  }
72
- #buffered = [];
73
67
  #slider = renderSlider();
74
68
  connectedCallback() {
75
- this.upgradeProperties("buffered", "groovePress", "modulation", "orientation", "spring");
69
+ this.upgradeProperties("groovePress", "modulationValue", "modulation", "orientation", "spring");
76
70
  super.connectedCallback();
77
71
  }
78
72
  connect(signal) {
79
73
  const slider = this.#slider;
80
74
  const cap = requireChild(slider, ".sonic-slider-cap", HTMLDivElement);
81
75
  this.bindScale(slider, requireChild(slider, ".sonic-slider-scale", HTMLDivElement), signal);
76
+ slider.addEventListener("transitionend", (event) => {
77
+ if (event.propertyName === "--_sonic-slider-value") this.toggleState("springing", false);
78
+ }, { signal });
82
79
  this.render();
83
80
  this.checkStyles(slider, "slider.css");
84
81
  this.bindGestures(slider, signal, (event) => {
85
82
  const axis = this.#axis(slider, cap);
86
83
  if (event.target instanceof Node && cap.contains(event.target)) return axis;
87
84
  if (this.groovePress === "none") return;
88
- this.input(this.valueAt((axis.position(event) - axis.startPx) / axis.travelPx));
85
+ this.input(this.mapping().valueAt((axis.position(event) - axis.startPx) / axis.travelPx));
89
86
  return axis;
90
87
  });
91
88
  }
89
+ controlOrientation() {
90
+ return this.orientation;
91
+ }
92
+ draw() {
93
+ this.writeProportions(this.#slider, "slider", this.modulation);
94
+ this.#renderBuffered(this.#slider);
95
+ }
92
96
  focusTarget() {
93
97
  return this.#slider;
94
98
  }
95
- render() {
96
- const slider = this.#slider;
97
- const positions = this.positions();
98
- const [modulationFrom, modulationTo] = this.modulationFractions(this.modulation);
99
- slider.style.setProperty("--_sonic-slider-value", String(this.fraction(this.value)));
100
- slider.style.setProperty("--_sonic-slider-origin", String(this.originFraction()));
101
- slider.style.setProperty("--_sonic-slider-modulation-from", String(modulationFrom));
102
- slider.style.setProperty("--_sonic-slider-modulation-to", String(modulationTo));
103
- if (positions === void 0) slider.style.removeProperty("--_sonic-slider-positions");
104
- else slider.style.setProperty("--_sonic-slider-positions", String(positions));
105
- this.#renderBuffered(slider);
106
- this.renderScale();
107
- this.renderAria(slider, this.orientation);
108
- }
109
99
  springTarget() {
110
- return this.spring ? this.restValue() : void 0;
100
+ return this.spring ? this.originValue() : void 0;
111
101
  }
112
102
  #axis(slider, cap) {
113
- const track = slider.getBoundingClientRect();
103
+ const box = slider.getBoundingClientRect();
114
104
  const capBox = cap.getBoundingClientRect();
115
- if (this.orientation === "vertical") return {
105
+ return this.orientation === "vertical" ? {
116
106
  position: (event) => -event.clientY,
117
- startPx: capBox.height / 2 - track.bottom,
118
- travelPx: Math.max(1, track.height - capBox.height)
119
- };
120
- return {
107
+ startPx: capBox.height / 2 - box.bottom,
108
+ travelPx: Math.max(1, box.height - capBox.height)
109
+ } : {
121
110
  position: (event) => event.clientX,
122
- startPx: track.left + capBox.width / 2,
123
- travelPx: Math.max(1, track.width - capBox.width)
111
+ startPx: box.left + capBox.width / 2,
112
+ travelPx: Math.max(1, box.width - capBox.width)
124
113
  };
125
114
  }
126
115
  #renderBuffered(slider) {
127
- if (this.#buffered.length === 0) {
128
- slider.style.removeProperty("--_sonic-slider-buffered-ranges");
116
+ const buffered = this.buffered;
117
+ if (buffered.length === 0) {
118
+ slider.style.removeProperty("--_sonic-slider-buffered-regions");
129
119
  return;
130
120
  }
131
- const stops = this.#buffered.flatMap(([start, end]) => {
132
- const from = grooveStop(this.fraction(Math.min(start, end)));
133
- const to = grooveStop(this.fraction(Math.max(start, end)));
121
+ const { proportionOf } = this.mapping();
122
+ const stops = buffered.flatMap(([start, end]) => {
123
+ const from = grooveStop(proportionOf(Math.min(start, end)));
124
+ const to = grooveStop(proportionOf(Math.max(start, end)));
134
125
  return [
135
126
  `transparent ${from}`,
136
127
  `var(--_sonic-slider-buffered) ${from} ${to}`,
137
128
  `transparent ${to}`
138
129
  ];
139
130
  });
140
- slider.style.setProperty("--_sonic-slider-buffered-ranges", `linear-gradient(var(--_sonic-slider-toward), ${stops.join(", ")})`);
131
+ slider.style.setProperty("--_sonic-slider-buffered-regions", `linear-gradient(var(--_sonic-slider-toward), ${stops.join(", ")})`);
141
132
  }
142
133
  };
143
134
 
@@ -1,4 +1,5 @@
1
- import { attachSlot } from "../lib/render.js";
1
+ import { toNumber } from "../lib/math.js";
2
+ import { attachSlots } from "../lib/render.js";
2
3
  import { writeAttribute } from "../lib/write-attribute.js";
3
4
 
4
5
  //#region src/elements/sonic-element.ts
@@ -22,7 +23,7 @@ var SonicElement = class extends HTMLElement {
22
23
  }
23
24
  internals = "attachInternals" in HTMLElement.prototype ? this.attachInternals() : void 0;
24
25
  #connection;
25
- #slot;
26
+ #slots;
26
27
  blur() {
27
28
  this.#focused()?.blur();
28
29
  }
@@ -30,13 +31,14 @@ var SonicElement = class extends HTMLElement {
30
31
  this.upgradeProperties("disabled");
31
32
  if (this.#connection) return;
32
33
  const connection = new AbortController();
34
+ this.#connection = connection;
33
35
  try {
34
36
  this.connect(connection.signal);
35
37
  } catch (error) {
36
38
  connection.abort();
39
+ this.#connection = void 0;
37
40
  throw error;
38
41
  }
39
- this.#connection = connection;
40
42
  }
41
43
  connectedMoveCallback() {}
42
44
  disconnectedCallback() {
@@ -56,9 +58,6 @@ var SonicElement = class extends HTMLElement {
56
58
  }
57
59
  if (!this.isDisabled() && !this.#focused()) target.focus(options);
58
60
  }
59
- appendOnce(child) {
60
- if (child.parentNode !== this) this.append(child);
61
- }
62
61
  checkStyles(control, sheet) {}
63
62
  focusTarget() {}
64
63
  forwardNaming(target, isNamed) {
@@ -70,28 +69,64 @@ var SonicElement = class extends HTMLElement {
70
69
  }
71
70
  if (labels.length > 0) writeLabelledBy(target, labels);
72
71
  }
72
+ isBound() {
73
+ return this.#connection !== void 0;
74
+ }
73
75
  isDisabled() {
74
76
  return this.disabled || this.matches(":disabled");
75
77
  }
76
- mirrorChildren({ control, copy, isCopied = () => true }, signal) {
77
- const slot = this.#slot ?? attachSlot(this);
78
- this.#slot = slot;
79
- const mirror = () => {
78
+ keepControl(control, signal) {
79
+ const keep = () => {
80
+ this.#appendOnce(control);
81
+ this.#checkChildren([...this.childNodes].filter((child) => child !== control));
82
+ };
83
+ const observer = new MutationObserver(keep);
84
+ keep();
85
+ observer.observe(this, { childList: true });
86
+ signal.addEventListener("abort", () => {
87
+ observer.disconnect();
88
+ }, { once: true });
89
+ }
90
+ mirrorChildren({ control, copy, isCopied = () => true, isPassed = () => false, place }, signal) {
91
+ const slots = this.#slots ?? attachSlots(this);
92
+ let copies = /* @__PURE__ */ new Map();
93
+ this.#slots = slots;
94
+ const mirror = (touched) => {
95
+ this.#appendOnce(control);
80
96
  const children = [...this.childNodes].filter((child) => child !== control);
81
- copy(children.filter((child) => isCopied(child)));
82
- this.appendOnce(control);
83
- slot.assign(...children.filter((child) => (child instanceof Element || child instanceof Text) && !isCopied(child)), control);
97
+ const slotted = children.filter((child) => child instanceof Element || child instanceof Text);
98
+ const kept = copies;
99
+ const originals = children.filter((child) => isCopied(child));
100
+ this.#checkMirrored(originals);
101
+ this.#checkChildren(children.filter((child) => !isCopied(child) && !isPassed(child)));
102
+ copies = new Map(originals.map((original) => [original, touched && !touched.has(original) && kept.has(original) ? kept.get(original) : copy(original)]));
103
+ place([...copies.values()].filter((made) => made !== void 0));
104
+ slots.shown.assign(...slotted.filter((child) => !isCopied(child)), control);
105
+ slots.kept.assign(...slotted.filter((child) => isCopied(child)));
106
+ };
107
+ const watch = () => {
108
+ observer.disconnect();
109
+ observer.observe(this, { childList: true });
110
+ for (const child of this.childNodes) {
111
+ if (child === control) continue;
112
+ observer.observe(child, {
113
+ attributes: true,
114
+ characterData: true,
115
+ childList: true,
116
+ subtree: true
117
+ });
118
+ }
84
119
  };
85
120
  const observer = new MutationObserver((records) => {
86
- if (records.some((record) => this.#isCopiedChange(record, control, isCopied))) mirror();
121
+ const changes = records.filter((record) => this.#isCopiedChange(record, control, isCopied));
122
+ if (changes.length === 0) return;
123
+ if (changes.some((record) => record.target === this)) {
124
+ mirror();
125
+ watch();
126
+ } else mirror(new Set(changes.map((record) => this.#childHolding(record.target))));
87
127
  });
88
128
  mirror();
89
- observer.observe(this, {
90
- attributes: true,
91
- characterData: true,
92
- childList: true,
93
- subtree: true
94
- });
129
+ watch();
95
130
  signal.addEventListener("abort", () => {
96
131
  observer.disconnect();
97
132
  }, { once: true });
@@ -100,9 +135,7 @@ var SonicElement = class extends HTMLElement {
100
135
  return [];
101
136
  }
102
137
  numberAttribute(name, fallback) {
103
- const attribute = this.getAttribute(name);
104
- if (attribute === null) return fallback;
105
- const parsed = Number(attribute);
138
+ const parsed = toNumber(this.getAttribute(name));
106
139
  return Number.isFinite(parsed) ? parsed : fallback;
107
140
  }
108
141
  optionalNumberAttribute(name) {
@@ -110,11 +143,11 @@ var SonicElement = class extends HTMLElement {
110
143
  return Number.isNaN(value) ? void 0 : value;
111
144
  }
112
145
  reflect(name, value) {
113
- if (value === true) {
114
- this.setAttribute(name, "");
146
+ if (typeof value !== "number" && typeof value !== "string" && value !== true) {
147
+ this.removeAttribute(name);
115
148
  return;
116
149
  }
117
- writeAttribute(this, name, value === false || value === null ? void 0 : value?.toString());
150
+ this.setAttribute(name, value === true ? "" : value.toString());
118
151
  }
119
152
  toggleState(state, isOn) {
120
153
  if (isOn) this.internals?.states.add(state);
@@ -128,6 +161,14 @@ var SonicElement = class extends HTMLElement {
128
161
  this[name] = value;
129
162
  }
130
163
  }
164
+ #appendOnce(control) {
165
+ const [hook] = control.classList;
166
+ const stale = [...this.children].filter((child) => child !== control && hook !== void 0 && child.classList.contains(hook));
167
+ for (const child of stale) child.remove();
168
+ if (control.parentNode !== this) this.append(control);
169
+ }
170
+ #checkChildren(undocumented) {}
171
+ #checkMirrored(originals) {}
131
172
  #childHolding(node) {
132
173
  let current = node;
133
174
  while (current && current.parentNode !== this) current = current.parentNode;
@@ -139,12 +180,9 @@ var SonicElement = class extends HTMLElement {
139
180
  return active instanceof HTMLElement && this.contains(active) ? active : void 0;
140
181
  }
141
182
  #isCopiedChange(record, control, isCopied) {
142
- if (record.target === this) {
143
- if (record.type === "attributes") return false;
144
- return record.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control);
145
- }
183
+ if (record.target === this) return record.removedNodes.length > 0 || [...record.addedNodes].some((node) => node !== control);
146
184
  const child = this.#childHolding(record.target);
147
- if (child === void 0 || child === control) return false;
185
+ if (child === void 0) return false;
148
186
  return isCopied(child) || record.type === "attributes" && record.target === child;
149
187
  }
150
188
  };
@@ -0,0 +1,249 @@
1
+ import { SonicElement } from "./sonic-element.js";
2
+ import { SonicValueElement, linkValue } from "./value-element.js";
3
+ import { distribute, splitLimits } from "../lib/distribute.js";
4
+
5
+ //#region src/elements/split.ts
6
+ const modeNames = /* @__PURE__ */ new Set([
7
+ "cascade",
8
+ "equal",
9
+ "proportional"
10
+ ]);
11
+ const gridSlop = 1e-9;
12
+ const lockAttributes = ["data-sonic-locked", "disabled"];
13
+ function isMode(name) {
14
+ return name !== null && modeNames.has(name);
15
+ }
16
+ function isLocked(member) {
17
+ return "sonicLocked" in member.element.dataset || member.link.isDisabled();
18
+ }
19
+ function gridLimits(mapping, [low, high]) {
20
+ const bottom = mapping.snap(low);
21
+ const top = mapping.snap(high);
22
+ return [bottom < low - gridSlop ? mapping.keyTarget("ArrowRight", bottom) ?? bottom : bottom, top > high + gridSlop ? mapping.keyTarget("ArrowLeft", top) ?? top : top];
23
+ }
24
+ function modelOf(member, value, isFree) {
25
+ const mapping = member.link.mapping();
26
+ const [first, last] = mapping.bounds;
27
+ return {
28
+ isFree,
29
+ max: Math.max(first, last),
30
+ min: Math.min(first, last),
31
+ snap: mapping.snap,
32
+ stepFrom: (from, direction) => mapping.keyTarget(direction > 0 ? "ArrowRight" : "ArrowLeft", from) ?? from,
33
+ value
34
+ };
35
+ }
36
+ var SonicSplit = class extends SonicElement {
37
+ static observedAttributes = [
38
+ ...SonicElement.observedAttributes,
39
+ "mode",
40
+ "total"
41
+ ];
42
+ get mode() {
43
+ const name = this.getAttribute("mode");
44
+ return isMode(name) ? name : "proportional";
45
+ }
46
+ set mode(name) {
47
+ this.reflect("mode", name);
48
+ }
49
+ get total() {
50
+ return this.optionalNumberAttribute("total");
51
+ }
52
+ set total(value) {
53
+ this.reflect("total", value);
54
+ }
55
+ #isQueued = false;
56
+ #isTotalKept = false;
57
+ #isWarned = false;
58
+ #isWriting = false;
59
+ #members = [];
60
+ #signal;
61
+ #snapshot;
62
+ #total = 0;
63
+ #waiting = /* @__PURE__ */ new Set();
64
+ attributeChangedCallback(name) {
65
+ if ((name === "mode" || name === "total") && this.#signal) this.#bind();
66
+ }
67
+ connectedCallback() {
68
+ this.upgradeProperties("mode", "total");
69
+ super.connectedCallback();
70
+ }
71
+ connect(signal) {
72
+ const observer = new MutationObserver((records) => {
73
+ if (records.some((record) => record.attributeName !== null && lockAttributes.includes(record.attributeName)) || !this.#isSameMembers(this.#find())) this.#bind();
74
+ });
75
+ this.#signal = signal;
76
+ if (this.internals) this.internals.role = "group";
77
+ observer.observe(this, {
78
+ attributeFilter: lockAttributes,
79
+ attributes: true,
80
+ childList: true,
81
+ subtree: true
82
+ });
83
+ this.addEventListener("change", (event) => {
84
+ this.#release(event);
85
+ }, { signal });
86
+ signal.addEventListener("abort", () => {
87
+ observer.disconnect();
88
+ this.#signal = void 0;
89
+ this.#isTotalKept = false;
90
+ this.#unbind();
91
+ }, { once: true });
92
+ this.#bind();
93
+ }
94
+ #bind() {
95
+ const known = new Set(this.#members.map(({ element }) => element));
96
+ this.#unbind();
97
+ this.#members = this.#find().map((element) => this.#join(element));
98
+ const values = this.#members.map(({ seen }) => seen);
99
+ const sum = values.reduce((total, value) => total + value, 0);
100
+ this.#total = this.total ?? (this.#isTotalKept ? this.#total : sum);
101
+ this.#isTotalKept = this.#members.length > 0;
102
+ if (this.#members.length > 0 && sum !== this.#total) this.#spread(values, known);
103
+ this.#limit();
104
+ this.#warnReach();
105
+ }
106
+ #compensate() {
107
+ this.#isQueued = false;
108
+ for (const mover of this.#members) if (mover.link.value() !== mover.seen) this.#move(mover);
109
+ }
110
+ #find() {
111
+ const found = [];
112
+ for (const element of this.querySelectorAll("*")) {
113
+ if (element.closest("sonic-split") !== this) continue;
114
+ if (element instanceof SonicValueElement) found.push(element);
115
+ else this.#waitFor(element.localName);
116
+ }
117
+ return found;
118
+ }
119
+ #isSameMembers(elements) {
120
+ return elements.length === this.#members.length && elements.every((element, index) => this.#members[index]?.element === element);
121
+ }
122
+ #join(element) {
123
+ const link = linkValue(element);
124
+ const unwatch = link.watch(() => {
125
+ if (this.#isWriting || this.#isQueued) return;
126
+ this.#isQueued = true;
127
+ queueMicrotask(() => {
128
+ if (this.#isQueued) this.#compensate();
129
+ });
130
+ });
131
+ link.land((target, direction) => this.#land(element, target, direction));
132
+ return {
133
+ element,
134
+ link,
135
+ seen: link.value(),
136
+ unwatch
137
+ };
138
+ }
139
+ #land(element, target, direction) {
140
+ if (this.#isQueued) this.#compensate();
141
+ const index = this.#members.findIndex((member) => member.element === element);
142
+ const mover = this.#members[index];
143
+ if (!mover) return target;
144
+ return distribute(this.#model(this.#members.map(({ seen }) => seen), mover), {
145
+ mode: this.mode,
146
+ total: this.#total
147
+ }, {
148
+ direction,
149
+ index,
150
+ target
151
+ })[index] ?? target;
152
+ }
153
+ #limit() {
154
+ const values = this.#members.map(({ seen }) => seen);
155
+ for (const [index, member] of this.#members.entries()) {
156
+ const model = this.#model(values, member);
157
+ const value = values[index] ?? member.seen;
158
+ member.link.limit(isLocked(member) ? [value, value] : gridLimits(member.link.mapping(), splitLimits(model, index, this.#total)));
159
+ }
160
+ }
161
+ #model(values, mover) {
162
+ return this.#members.map((member, index) => {
163
+ const isFixed = member !== mover && (isLocked(member) || member.link.isHeld());
164
+ return modelOf(member, values[index] ?? member.seen, !isFixed);
165
+ });
166
+ }
167
+ #move(mover) {
168
+ const index = this.#members.indexOf(mover);
169
+ const kept = this.#snapshot;
170
+ const snapshot = kept?.mover === mover && mover.link.isHeld() ? kept : {
171
+ mover,
172
+ values: this.#members.map(({ seen }) => seen)
173
+ };
174
+ this.#snapshot = snapshot;
175
+ this.#write(distribute(this.#model(snapshot.values, mover), {
176
+ mode: this.mode,
177
+ total: this.#total
178
+ }, {
179
+ index,
180
+ target: mover.link.value()
181
+ }), mover);
182
+ this.#limit();
183
+ }
184
+ #release(event) {
185
+ if (this.#isWriting) return;
186
+ if (this.#isQueued) this.#compensate();
187
+ const snapshot = this.#snapshot;
188
+ if (snapshot?.mover.element !== event.target) return;
189
+ this.#snapshot = void 0;
190
+ const changed = this.#members.filter((member, index) => member !== snapshot.mover && member.link.value() !== snapshot.values[index]);
191
+ queueMicrotask(() => {
192
+ this.#isWriting = true;
193
+ try {
194
+ for (const { element } of changed) element.dispatchEvent(new Event("change", { bubbles: true }));
195
+ } finally {
196
+ this.#isWriting = false;
197
+ }
198
+ });
199
+ }
200
+ #spread(values, known) {
201
+ const model = this.#model(values);
202
+ const joined = this.#members.map(({ element }) => known.size > 0 && !known.has(element));
203
+ const isAnyKnownFree = model.some((member, index) => member.isFree && joined[index] !== true);
204
+ const spreadOver = model.map((member, index) => isAnyKnownFree && joined[index] === true ? {
205
+ ...member,
206
+ isFree: false
207
+ } : member);
208
+ this.#write(distribute(spreadOver, {
209
+ mode: this.mode,
210
+ total: this.#total
211
+ }));
212
+ }
213
+ #unbind() {
214
+ for (const { link, unwatch } of this.#members) {
215
+ unwatch();
216
+ link.land(void 0);
217
+ link.limit(void 0);
218
+ }
219
+ this.#members = [];
220
+ this.#snapshot = void 0;
221
+ this.#isQueued = false;
222
+ }
223
+ #waitFor(tag) {
224
+ if (!tag.includes("-") || customElements.get(tag) || this.#waiting.has(tag)) return;
225
+ this.#waiting.add(tag);
226
+ customElements.whenDefined(tag).then(() => {
227
+ this.#waiting.delete(tag);
228
+ if (this.#signal) this.#bind();
229
+ });
230
+ }
231
+ #warnReach() {}
232
+ #write(values, mover) {
233
+ this.#isWriting = true;
234
+ try {
235
+ for (const [index, member] of this.#members.entries()) {
236
+ const value = values[index];
237
+ if (value === void 0 || value === member.link.value()) continue;
238
+ member.link.limit(void 0);
239
+ member.link.input(value, member === mover);
240
+ }
241
+ } finally {
242
+ this.#isWriting = false;
243
+ }
244
+ for (const member of this.#members) member.seen = member.link.value();
245
+ }
246
+ };
247
+
248
+ //#endregion
249
+ export { SonicSplit };