@xsynaptic/sonic-ui 0.27.0 → 0.28.1

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 (55) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +1 -0
  3. package/dist/default/define/ticker.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/sonic-element.js +1 -1
  6. package/dist/default/elements/ticker.js +331 -0
  7. package/dist/default/elements/wavestrip.js +2 -5
  8. package/dist/default/index.js +2 -1
  9. package/dist/default/lib/cancel-zone.js +12 -0
  10. package/dist/default/lib/distribute.js +2 -3
  11. package/dist/default/lib/mirror-children.js +1 -1
  12. package/dist/default/lib/pointer-drag.js +8 -2
  13. package/dist/default/lib/ticker-leg.js +54 -0
  14. package/dist/dev/define/ticker.d.ts +2 -0
  15. package/dist/dev/define/ticker.js +8 -0
  16. package/dist/dev/define/ticker.js.map +1 -0
  17. package/dist/dev/define.d.ts +3 -1
  18. package/dist/dev/define.js +1 -0
  19. package/dist/dev/elements/meter.d.ts +1 -0
  20. package/dist/dev/elements/position-group.d.ts +1 -0
  21. package/dist/dev/elements/sonic-element.d.ts +1 -0
  22. package/dist/dev/elements/sonic-element.js +3 -2
  23. package/dist/dev/elements/sonic-element.js.map +1 -1
  24. package/dist/dev/elements/spectrum.d.ts +1 -0
  25. package/dist/dev/elements/ticker.d.ts +25 -0
  26. package/dist/dev/elements/ticker.js +332 -0
  27. package/dist/dev/elements/ticker.js.map +1 -0
  28. package/dist/dev/elements/value-element.d.ts +1 -0
  29. package/dist/dev/elements/wavestrip.d.ts +1 -0
  30. package/dist/dev/elements/wavestrip.js +2 -5
  31. package/dist/dev/elements/wavestrip.js.map +1 -1
  32. package/dist/dev/index.d.ts +2 -1
  33. package/dist/dev/index.js +2 -1
  34. package/dist/dev/lib/cancel-zone.js +13 -0
  35. package/dist/dev/lib/cancel-zone.js.map +1 -0
  36. package/dist/dev/lib/check-styles.js +20 -12
  37. package/dist/dev/lib/check-styles.js.map +1 -1
  38. package/dist/dev/lib/crossfade.d.ts +1 -0
  39. package/dist/dev/lib/distribute.js +2 -3
  40. package/dist/dev/lib/distribute.js.map +1 -1
  41. package/dist/dev/lib/mirror-children.js +9 -7
  42. package/dist/dev/lib/mirror-children.js.map +1 -1
  43. package/dist/dev/lib/pointer-drag.js +8 -2
  44. package/dist/dev/lib/pointer-drag.js.map +1 -1
  45. package/dist/dev/lib/ticker-leg.js +55 -0
  46. package/dist/dev/lib/ticker-leg.js.map +1 -0
  47. package/dist/styles/controls.css +1 -0
  48. package/dist/styles/dial.css +28 -5
  49. package/dist/styles/material/cap.css +16 -5
  50. package/dist/styles/material/core.css +1 -0
  51. package/dist/styles/slider.css +2 -1
  52. package/dist/styles/switch.css +3 -1
  53. package/dist/styles/ticker.css +115 -0
  54. package/dist/styles/toggle.css +7 -5
  55. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @xsynaptic/sonic-ui
2
2
 
3
+ ## 0.28.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Ticker: setting `text` to the text it already shows no longer sends the tape back to the start, and `disabled` now holds the ticker at rest with no scrolling and no drag.
8
+
9
+ ## 0.28.0
10
+
11
+ ### Minor Changes
12
+
13
+ - `--sonic-gloss` scales a cap's sheen apart from `--sonic-relief`: 0 is matte, 1 the default. A slider cap has no sheen until the token is set, so there `1` is not the same as unset.
14
+ - - `<sonic-ticker>`: one line of content that scrolls there and back when it does not fit its box, registered by `./define/ticker` with `ticker.css`, which needs no material sheet. It stands still with an ellipsis under reduced motion, and pauses under a mouse, while a link round it has keyboard focus, off screen, and while `paused` is set. A drag along the line moves the tape; with `cancellable` a drag that strays past `--sonic-cancel-zone`, shared with the wave strip, puts it back. `text` replaces the content. A link among the children is copied and still navigates.
15
+ - Ticker surface: tokens `--sonic-ticker-rate` (a length a second, `40px`), `--sonic-ticker-hold` (`2s`) and `--sonic-ticker-fade` (`0.75rem`); hook classes `.sonic-ticker` and `.sonic-ticker-tape`; states `data-sonic-overflowing`, `data-sonic-dragging`, `data-sonic-cancelling` and `data-sonic-paused`, and `data-sonic-clipped` naming each end with content past it.
16
+
17
+ ### Patch Changes
18
+
19
+ - Switch and toggle: the bat and the cap land with a small overshoot, and the switch's flip takes 120ms, up from 90ms.
20
+ - Split: a move the siblings cannot absorb on their own steps is turned back, so the total holds across mixed step grids.
21
+
3
22
  ## 0.27.0
4
23
 
5
24
  ### Minor Changes
package/README.md CHANGED
@@ -88,6 +88,7 @@ A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to st
88
88
  | `slider.css` | core, cap, glass, readout, value, groove, scale |
89
89
  | `spectrum.css` | core |
90
90
  | `switch.css` | core, cap |
91
+ | `ticker.css` | |
91
92
  | `toggle.css` | core, cap, well, keycap |
92
93
  | `waveform.css` | core, cap, glass, readout, value |
93
94
  | `wavestrip.css` | core, cap, glass, readout, value |
@@ -0,0 +1,7 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicTicker } from "../elements/ticker.js";
3
+
4
+ //#region src/define/ticker.ts
5
+ defineOnce("sonic-ticker", SonicTicker);
6
+
7
+ //#endregion
@@ -9,6 +9,7 @@ import "./define/slider.js";
9
9
  import "./define/spectrum.js";
10
10
  import "./define/split.js";
11
11
  import "./define/switch.js";
12
+ import "./define/ticker.js";
12
13
  import "./define/toggle.js";
13
14
  import "./define/waveform.js";
14
15
  import "./define/wavestrip.js";
@@ -48,7 +48,7 @@ var SonicElement = class SonicElement extends HTMLElement {
48
48
  this.#connection = void 0;
49
49
  throw error;
50
50
  }
51
- const { boxProbe: box, control, sheet } = this;
51
+ const { boxProbe: box, control, sheet, sheetToken: token } = this;
52
52
  }
53
53
  connectedMoveCallback() {}
54
54
  disconnectedCallback() {
@@ -0,0 +1,331 @@
1
+ import { clamp } from "../lib/math.js";
2
+ import { writeAttribute } from "../lib/write-attribute.js";
3
+ import { SonicElement } from "./sonic-element.js";
4
+ import { copyNode } from "../lib/copy-node.js";
5
+ import { placeChildren, requireChild, template } from "../lib/render.js";
6
+ import { mirrorChildren } from "../lib/mirror-children.js";
7
+ import { bindDrag, capturePointer } from "../lib/pointer-drag.js";
8
+ import { readMsProperty } from "../lib/read-ms-property.js";
9
+ import { dragThresholdPx } from "../lib/drag-step.js";
10
+ import { readPxProperty } from "../lib/read-px-property.js";
11
+ import { outsideCancelZone } from "../lib/cancel-zone.js";
12
+ import { clippedEnds, fadeWidth, legDeparture, legFades, legPosition, tickerLeg } from "../lib/ticker-leg.js";
13
+
14
+ //#region src/elements/ticker.ts
15
+ const renderTicker = template(`<span class="sonic-ticker"><span class="sonic-ticker-tape"></span></span>`, HTMLSpanElement);
16
+ const fadePx = 12;
17
+ const holdMs = 2e3;
18
+ const ratePxPerSecond = 40;
19
+ var SonicTicker = class extends SonicElement {
20
+ static observedAttributes = ["disabled", "paused"];
21
+ get cancellable() {
22
+ return this.hasAttribute("cancellable");
23
+ }
24
+ set cancellable(isCancellable) {
25
+ this.reflect("cancellable", isCancellable);
26
+ }
27
+ get paused() {
28
+ return this.hasAttribute("paused");
29
+ }
30
+ set paused(isPaused) {
31
+ this.reflect("paused", isPaused);
32
+ }
33
+ get text() {
34
+ return this.#originals().map((original) => original.textContent).join("");
35
+ }
36
+ set text(text) {
37
+ const next = text ?? "";
38
+ if (this.#holds(next)) return;
39
+ for (const original of this.#originals()) original.remove();
40
+ if (next !== "") this.prepend(next);
41
+ }
42
+ control = renderTicker();
43
+ sheet = "ticker.css";
44
+ sheetToken = "--_sonic-ticker-fade";
45
+ #drag;
46
+ #fade = fadePx;
47
+ #heading = 1;
48
+ #isClickSwallowed = false;
49
+ #isFocusInside = false;
50
+ #isHovered = false;
51
+ #isOffScreen = false;
52
+ #isPressed = false;
53
+ #mark;
54
+ #measured = "";
55
+ #position = 0;
56
+ #reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
57
+ #running;
58
+ #tape = requireChild(this.control, ".sonic-ticker-tape", HTMLSpanElement);
59
+ #travel = {
60
+ distance: 0,
61
+ hold: holdMs,
62
+ rate: ratePxPerSecond
63
+ };
64
+ attributeChangedCallback(name) {
65
+ if (name === "disabled" && this.isBound()) {
66
+ this.#drag?.end();
67
+ this.#run(this.#tape, {
68
+ from: 0,
69
+ heading: 1
70
+ });
71
+ }
72
+ this.render();
73
+ }
74
+ connect(signal) {
75
+ const control = this.control;
76
+ const tape = this.#tape;
77
+ const measure = () => {
78
+ this.#measure(tape);
79
+ };
80
+ const observer = new ResizeObserver(measure);
81
+ const drag = this.#bindDrag(tape, signal);
82
+ this.#drag = drag;
83
+ this.upgradeProperties("cancellable", "paused", "text");
84
+ mirrorChildren(this, {
85
+ canCopyLinks: true,
86
+ control,
87
+ copy: copyNode,
88
+ place: (copies) => {
89
+ placeChildren(tape, copies);
90
+ drag.end();
91
+ this.#measure(tape, {
92
+ from: 0,
93
+ heading: 1
94
+ });
95
+ }
96
+ }, signal);
97
+ this.render();
98
+ observer.observe(control);
99
+ observer.observe(tape);
100
+ if ("fonts" in document) document.fonts.addEventListener("loadingdone", measure, { signal });
101
+ this.#bindFocus(tape, signal);
102
+ this.#bindPauses(signal);
103
+ this.#reducedMotion.addEventListener("change", () => {
104
+ this.#run(tape);
105
+ }, { signal });
106
+ signal.addEventListener("abort", () => {
107
+ observer.disconnect();
108
+ this.#stop();
109
+ }, { once: true });
110
+ }
111
+ render() {
112
+ if (!this.isBound()) return;
113
+ this.toggleState("paused", this.paused);
114
+ this.#syncPause();
115
+ }
116
+ #bindDrag(tape, signal) {
117
+ const control = this.control;
118
+ control.addEventListener("click", (event) => {
119
+ if (!this.#isClickSwallowed) return;
120
+ this.#isClickSwallowed = false;
121
+ event.preventDefault();
122
+ event.stopPropagation();
123
+ }, {
124
+ capture: true,
125
+ signal
126
+ });
127
+ this.getRootNode().addEventListener("dragstart", (event) => {
128
+ if (this.#isPressed) event.preventDefault();
129
+ }, { signal });
130
+ return bindDrag(control, {
131
+ grab: (event) => {
132
+ if (this.#travel.distance <= 0 || this.isDisabled()) return;
133
+ return {
134
+ from: 0,
135
+ isDragging: false,
136
+ outside: void 0,
137
+ thresholdPx: dragThresholdPx(event.pointerType),
138
+ x: event.clientX
139
+ };
140
+ },
141
+ isCaptureLate: true,
142
+ move: (held, event) => {
143
+ if (!held.isDragging) {
144
+ if (Math.abs(event.clientX - held.x) < held.thresholdPx) return;
145
+ const { from, heading } = this.#where();
146
+ held.isDragging = true;
147
+ held.from = from;
148
+ held.outside = this.cancellable ? outsideCancelZone(control, "--_sonic-ticker-cancel-zone") : void 0;
149
+ held.x = event.clientX;
150
+ capturePointer(control, event.pointerId);
151
+ this.toggleState("dragging", true);
152
+ this.#stop();
153
+ this.#heading = heading;
154
+ }
155
+ const isOutside = held.outside?.(event) === true;
156
+ this.toggleState("cancelling", isOutside);
157
+ this.#place(tape, isOutside ? held.from : held.from - (event.clientX - held.x));
158
+ },
159
+ release: (held) => {
160
+ if (!held.isDragging) return;
161
+ this.#isClickSwallowed = true;
162
+ setTimeout(() => {
163
+ this.#isClickSwallowed = false;
164
+ }, 0);
165
+ this.toggleState("dragging", false);
166
+ this.toggleState("cancelling", false);
167
+ this.#run(tape);
168
+ },
169
+ toggle: (isPressed) => {
170
+ this.#isPressed = isPressed;
171
+ this.#syncPause();
172
+ }
173
+ }, signal);
174
+ }
175
+ #bindFocus(tape, signal) {
176
+ const control = this.control;
177
+ control.addEventListener("focusout", (event) => {
178
+ if (event.relatedTarget instanceof Node && control.contains(event.relatedTarget)) return;
179
+ control.scrollLeft = 0;
180
+ this.#isFocusInside = false;
181
+ this.#run(tape);
182
+ }, { signal });
183
+ control.addEventListener("focusin", (event) => {
184
+ this.#isFocusInside = true;
185
+ this.#run(tape);
186
+ if (event.target instanceof Element) event.target.scrollIntoView({ block: "nearest" });
187
+ }, { signal });
188
+ }
189
+ #bindPauses(signal) {
190
+ const control = this.control;
191
+ for (const type of ["pointerenter", "pointerleave"]) control.addEventListener(type, (event) => {
192
+ if (event.pointerType !== "mouse") return;
193
+ this.#isHovered = type === "pointerenter";
194
+ this.#syncPause();
195
+ }, { signal });
196
+ for (const type of ["focusin", "focusout"]) this.getRootNode().addEventListener(type, () => {
197
+ this.#syncPause();
198
+ }, { signal });
199
+ const view = new IntersectionObserver((entries) => {
200
+ this.#isOffScreen = entries.at(-1)?.isIntersecting === false;
201
+ this.#syncPause();
202
+ });
203
+ view.observe(control);
204
+ signal.addEventListener("abort", () => {
205
+ view.disconnect();
206
+ }, { once: true });
207
+ }
208
+ #holds(text) {
209
+ const [only, ...rest] = this.#originals();
210
+ if (only === void 0) return text === "";
211
+ if (rest.length > 0 || !(only instanceof Text)) return false;
212
+ return only.data === text;
213
+ }
214
+ #isPaused() {
215
+ return this.paused || this.#isHovered || this.#isPressed || this.#isOffScreen || this.control.closest(":focus-visible") !== null;
216
+ }
217
+ #measure(tape, departure) {
218
+ const control = this.control;
219
+ const styles = getComputedStyle(control);
220
+ const box = control.clientWidth - readPxProperty(styles, "padding-left", 0) - readPxProperty(styles, "padding-right", 0);
221
+ const travel = {
222
+ distance: Math.max(0, Math.round(tape.offsetWidth - box)),
223
+ hold: readMsProperty(styles, "--_sonic-ticker-hold", holdMs),
224
+ rate: readPxProperty(styles, "--_sonic-ticker-rate", ratePxPerSecond)
225
+ };
226
+ const fade = readPxProperty(styles, "--_sonic-ticker-fade", fadePx);
227
+ const measured = [...Object.values(travel), fade].join(" ");
228
+ if (!departure && measured === this.#measured) return;
229
+ const from = departure ?? this.#where();
230
+ this.#fade = fade;
231
+ this.#measured = measured;
232
+ this.#travel = travel;
233
+ this.toggleState("overflowing", travel.distance > 0);
234
+ this.#run(tape, from);
235
+ }
236
+ #originals() {
237
+ return [...this.childNodes].filter((child) => child !== this.control);
238
+ }
239
+ #place(tape, position) {
240
+ const { distance } = this.#travel;
241
+ this.#position = clamp(position, 0, distance);
242
+ tape.style.translate = this.#position > 0 ? `${String(-this.#position)}px` : "";
243
+ writeAttribute(this.control, "data-sonic-clipped", clippedEnds(this.#position, distance));
244
+ this.#writeFades(this.#position);
245
+ }
246
+ #run(tape, departure = this.#where()) {
247
+ const leg = !this.#isFocusInside && !this.#reducedMotion.matches && !this.hasState("dragging") && !this.isDisabled() ? tickerLeg(this.#travel, departure) : void 0;
248
+ this.#stop();
249
+ this.#heading = departure.heading;
250
+ if (leg) this.#start(tape, leg);
251
+ else this.#place(tape, this.#isFocusInside ? 0 : departure.from);
252
+ }
253
+ #start(tape, leg) {
254
+ const animation = tape.animate({ translate: [`${String(-leg.from)}px`, `${String(-leg.to)}px`] }, {
255
+ delay: leg.hold,
256
+ duration: leg.duration,
257
+ fill: "both"
258
+ });
259
+ animation.addEventListener("finish", () => {
260
+ this.#run(tape, {
261
+ from: leg.to,
262
+ heading: leg.heading
263
+ });
264
+ });
265
+ this.#running = {
266
+ animation,
267
+ fades: [],
268
+ leg
269
+ };
270
+ tape.style.translate = "";
271
+ writeAttribute(this.control, "data-sonic-clipped", clippedEnds(leg.from, this.#travel.distance));
272
+ this.#syncPause();
273
+ }
274
+ #startFades(leg, elapsed) {
275
+ const fades = legFades(leg, this.#travel.distance, this.#fade);
276
+ return ["start", "end"].flatMap((end) => {
277
+ const { delay, duration, widths } = fades[end];
278
+ if (duration <= 0 || delay + duration <= elapsed) return [];
279
+ return this.control.animate({ [`--_sonic-ticker-fade-${end}`]: widths.map((width) => `${String(width)}px`) }, {
280
+ delay: delay - elapsed,
281
+ duration,
282
+ fill: "both"
283
+ });
284
+ });
285
+ }
286
+ #stop() {
287
+ const running = this.#running;
288
+ clearTimeout(this.#mark);
289
+ if (!running) return;
290
+ for (const fade of running.fades) fade.cancel();
291
+ running.animation.cancel();
292
+ this.#running = void 0;
293
+ }
294
+ #syncPause() {
295
+ const running = this.#running;
296
+ if (!running) return;
297
+ const { animation, leg } = running;
298
+ const elapsed = Number(animation.currentTime);
299
+ clearTimeout(this.#mark);
300
+ for (const fade of running.fades) fade.cancel();
301
+ running.fades = [];
302
+ this.#writeFades(legPosition(leg, elapsed));
303
+ if (elapsed >= leg.hold) writeAttribute(this.control, "data-sonic-clipped", "start end");
304
+ if (this.#isPaused()) {
305
+ animation.pause();
306
+ return;
307
+ }
308
+ if (animation.playState === "paused") animation.play();
309
+ running.fades = this.#startFades(leg, elapsed);
310
+ if (elapsed >= leg.hold) return;
311
+ this.#mark = setTimeout(() => {
312
+ writeAttribute(this.control, "data-sonic-clipped", "start end");
313
+ }, leg.hold - elapsed);
314
+ }
315
+ #where() {
316
+ const running = this.#running;
317
+ if (!running) return {
318
+ from: this.#position,
319
+ heading: this.#heading
320
+ };
321
+ return legDeparture(running.leg, Number(running.animation.currentTime));
322
+ }
323
+ #writeFades(position) {
324
+ const { style } = this.control;
325
+ style.setProperty("--_sonic-ticker-fade-start", `${String(fadeWidth(position, this.#fade))}px`);
326
+ style.setProperty("--_sonic-ticker-fade-end", `${String(fadeWidth(this.#travel.distance - position, this.#fade))}px`);
327
+ }
328
+ };
329
+
330
+ //#endregion
331
+ export { SonicTicker };
@@ -1,8 +1,8 @@
1
1
  import { clamp, clampProportion } from "../lib/math.js";
2
2
  import { requireChild, template } from "../lib/render.js";
3
- import { readPxProperty } from "../lib/read-px-property.js";
4
3
  import { scrubRegions } from "../lib/scrub.js";
5
4
  import { sortedRegions } from "../lib/time-regions.js";
5
+ import { outsideCancelZone } from "../lib/cancel-zone.js";
6
6
  import { SonicWaveElement } from "./wave-element.js";
7
7
  import { createMarkerBand } from "../lib/marker-band.js";
8
8
  import { RegionLayer } from "./region-layer.js";
@@ -22,7 +22,6 @@ const lengths = {
22
22
  pitch: "--_sonic-wavestrip-bar-pitch",
23
23
  radiusRatio: "--_sonic-wavestrip-bar-radius-ratio"
24
24
  };
25
- const cancelZonePx = 48;
26
25
  const renderWavestrip = template(`
27
26
  <div class="sonic-wavestrip">
28
27
  <canvas class="sonic-wavestrip-canvas" role="slider" tabindex="0"></canvas>
@@ -256,9 +255,7 @@ var SonicWavestrip = class extends SonicWaveElement {
256
255
  }
257
256
  #outside() {
258
257
  if (!this.cancellable) return void 0;
259
- const box = this.control.getBoundingClientRect();
260
- const zone = readPxProperty(getComputedStyle(this.control), "--_sonic-cancel-zone", cancelZonePx);
261
- return (event) => event.clientY < box.top - zone || event.clientY > box.bottom + zone;
258
+ return outsideCancelZone(this.control, "--_sonic-cancel-zone");
262
259
  }
263
260
  #placeReadout() {
264
261
  const at = this.mapping().proportionOf(this.readoutValue());
@@ -10,6 +10,7 @@ import { SonicSlider } from "./elements/slider.js";
10
10
  import { SonicSpectrum } from "./elements/spectrum.js";
11
11
  import { SonicSplit } from "./elements/split.js";
12
12
  import { SonicSwitch } from "./elements/switch.js";
13
+ import { SonicTicker } from "./elements/ticker.js";
13
14
  import { SonicToggle } from "./elements/toggle.js";
14
15
  import { formatClock, parseClock } from "./lib/clock.js";
15
16
  import { SonicWaveform } from "./elements/waveform.js";
@@ -18,4 +19,4 @@ import { SonicXy } from "./elements/xy.js";
18
19
  import { crossfadeGains } from "./lib/crossfade.js";
19
20
  import { formatPercent, parsePercent } from "./lib/percent.js";
20
21
 
21
- export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicRegion, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatClock, formatPercent, parseClock, parsePercent };
22
+ export { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicRegion, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicTicker, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy, crossfadeGains, envelopeCurve, formatClock, formatPercent, parseClock, parsePercent };
@@ -0,0 +1,12 @@
1
+ import { readPxProperty } from "./read-px-property.js";
2
+
3
+ //#region src/lib/cancel-zone.ts
4
+ const cancelZonePx = 48;
5
+ function outsideCancelZone(control, property) {
6
+ const box = control.getBoundingClientRect();
7
+ const zone = readPxProperty(getComputedStyle(control), property, cancelZonePx);
8
+ return (event) => event.clientY < box.top - zone || event.clientY > box.bottom + zone;
9
+ }
10
+
11
+ //#endregion
12
+ export { outsideCancelZone };
@@ -187,8 +187,7 @@ function close(members, rule, held) {
187
187
  const { landing, move, mover } = held;
188
188
  const { index } = move;
189
189
  const landed = landing.values[index] ?? mover.value;
190
- const targets = moverTargets(members, landing, index);
191
- return nearest(targets.flatMap((target) => {
190
+ return nearest(moverTargets(members, landing, index).flatMap((target) => {
192
191
  const attempt = spread(members, rule, {
193
192
  index,
194
193
  target
@@ -197,7 +196,7 @@ function close(members, rule, held) {
197
196
  }), move, {
198
197
  landed,
199
198
  start: mover.value
200
- }) ?? landing.values.with(index, targets[0] ?? landed);
199
+ }) ?? members.map(({ value }) => value);
201
200
  }
202
201
  function distribute(members, rule, move) {
203
202
  const landing = spread(members, rule, move);
@@ -14,7 +14,7 @@ function isCopiedChange(host, record, { control, isCopied }) {
14
14
  if (child === void 0) return false;
15
15
  return isCopied(child) || record.type === "attributes" && record.target === child;
16
16
  }
17
- function mirrorChildren(host, { control, copy, isCopied = () => true, isPassed = () => false, place }, signal) {
17
+ function mirrorChildren(host, { canCopyLinks = false, control, copy, isCopied = () => true, isPassed = () => false, place }, signal) {
18
18
  const slots = hostSlots.get(host) ?? attachSlots(host);
19
19
  let copies = /* @__PURE__ */ new Map();
20
20
  hostSlots.set(host, slots);
@@ -5,6 +5,7 @@ function capturePointer(target, pointerId) {
5
5
  } catch {}
6
6
  }
7
7
  function bindDrag(target, handlers, signal) {
8
+ const isCaptureLate = handlers.isCaptureLate === true;
8
9
  let held;
9
10
  const end = () => {
10
11
  if (!held) return;
@@ -28,14 +29,19 @@ function bindDrag(target, handlers, signal) {
28
29
  pointerId: event.pointerId
29
30
  };
30
31
  handlers.toggle?.(true);
31
- capturePointer(target, event.pointerId);
32
+ if (!isCaptureLate) capturePointer(target, event.pointerId);
32
33
  }, { signal });
33
34
  whileHeld("pointermove", handlers.move);
34
35
  whileHeld("pointercancel", (drag) => {
35
36
  handlers.cancel?.(drag);
36
37
  end();
37
38
  });
38
- whileHeld("lostpointercapture", end);
39
+ whileHeld("lostpointercapture", (_drag, event) => {
40
+ if (!isCaptureLate || event.target === target) end();
41
+ });
42
+ if (isCaptureLate) whileHeld("pointerleave", (_drag, event) => {
43
+ if (!target.hasPointerCapture(event.pointerId)) end();
44
+ });
39
45
  whileHeld("pointerup", (drag, event) => {
40
46
  end();
41
47
  handlers.lift?.(drag, event);
@@ -0,0 +1,54 @@
1
+ import { clamp, clampProportion } from "./math.js";
2
+
3
+ //#region src/lib/ticker-leg.ts
4
+ const fadeApproach = 3;
5
+ function tickerLeg({ distance, hold: fullHold, rate }, { from, heading, hold = fullHold }) {
6
+ if (!(distance > 0) || !(rate > 0)) return void 0;
7
+ const start = clamp(from, 0, distance);
8
+ const isOutbound = start === (heading > 0 ? distance : 0) ? heading < 0 : heading > 0;
9
+ const to = isOutbound ? distance : 0;
10
+ return {
11
+ duration: Math.abs(to - start) / rate * 1e3,
12
+ from: start,
13
+ heading: isOutbound ? 1 : -1,
14
+ hold: Math.max(0, hold),
15
+ to
16
+ };
17
+ }
18
+ function legPosition(leg, elapsed) {
19
+ return leg.from + (leg.to - leg.from) * clampProportion((elapsed - leg.hold) / leg.duration);
20
+ }
21
+ function legDeparture(leg, elapsed) {
22
+ return {
23
+ from: legPosition(leg, elapsed),
24
+ heading: leg.heading,
25
+ hold: Math.max(0, leg.hold - elapsed)
26
+ };
27
+ }
28
+ function clippedEnds(position, distance) {
29
+ const ends = [...position > 0 ? ["start"] : [], ...position < distance ? ["end"] : []];
30
+ return ends.length > 0 ? ends.join(" ") : void 0;
31
+ }
32
+ function fadeWidth(past, fade) {
33
+ return clamp(past / fadeApproach, 0, Math.max(0, fade));
34
+ }
35
+ function legFade(leg, [pastFrom, pastTo], fade) {
36
+ const reach = Math.max(0, fade) * fadeApproach;
37
+ const msPerPx = leg.duration / Math.abs(leg.to - leg.from);
38
+ const from = Math.min(pastFrom, reach);
39
+ const to = Math.min(pastTo, reach);
40
+ return {
41
+ delay: leg.hold + (pastFrom - from) * msPerPx,
42
+ duration: Math.abs(to - from) * msPerPx,
43
+ widths: [from / fadeApproach, to / fadeApproach]
44
+ };
45
+ }
46
+ function legFades(leg, distance, fade) {
47
+ return {
48
+ end: legFade(leg, [distance - leg.from, distance - leg.to], fade),
49
+ start: legFade(leg, [leg.from, leg.to], fade)
50
+ };
51
+ }
52
+
53
+ //#endregion
54
+ export { clippedEnds, fadeWidth, legDeparture, legFades, legPosition, tickerLeg };
@@ -0,0 +1,2 @@
1
+ import { SonicTicker } from "../elements/ticker.js";
2
+ export type { SonicTicker };
@@ -0,0 +1,8 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicTicker } from "../elements/ticker.js";
3
+
4
+ //#region src/define/ticker.ts
5
+ defineOnce("sonic-ticker", SonicTicker);
6
+
7
+ //#endregion
8
+ //# sourceMappingURL=ticker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ticker.js","names":[],"sources":["../../../src/define/ticker.ts"],"sourcesContent":["import { defineOnce } from '#elements/define-once.ts';\nimport { SonicTicker } from '#elements/ticker.ts';\n\ndefineOnce('sonic-ticker', SonicTicker);\n\nexport type { SonicTicker } from '#elements/ticker.ts';\n"],"mappings":";;;;AAGA,WAAW,gBAAgB,WAAW"}
@@ -20,6 +20,8 @@ import { SonicSplit } from "./elements/split.js";
20
20
  import "./define/split.js";
21
21
  import { SonicSwitch } from "./elements/switch.js";
22
22
  import "./define/switch.js";
23
+ import { SonicTicker } from "./elements/ticker.js";
24
+ import "./define/ticker.js";
23
25
  import { SonicToggle } from "./elements/toggle.js";
24
26
  import "./define/toggle.js";
25
27
  import { SonicWaveform } from "./elements/waveform.js";
@@ -28,4 +30,4 @@ import { SonicWavestrip } from "./elements/wavestrip.js";
28
30
  import "./define/wavestrip.js";
29
31
  import { SonicXy } from "./elements/xy.js";
30
32
  import "./define/xy.js";
31
- export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicRegion, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy };
33
+ export type { SonicButton, SonicDial, SonicEnvelope, SonicMeter, SonicNumber, SonicRegion, SonicSegmented, SonicSlider, SonicSpectrum, SonicSplit, SonicSwitch, SonicTicker, SonicToggle, SonicWaveform, SonicWavestrip, SonicXy };
@@ -9,6 +9,7 @@ import "./define/slider.js";
9
9
  import "./define/spectrum.js";
10
10
  import "./define/split.js";
11
11
  import "./define/switch.js";
12
+ import "./define/ticker.js";
12
13
  import "./define/toggle.js";
13
14
  import "./define/waveform.js";
14
15
  import "./define/wavestrip.js";
@@ -38,4 +38,5 @@ export declare class SonicMeter extends SonicElement {
38
38
  protected connect(signal: AbortSignal): void;
39
39
  }
40
40
  //#endregion
41
+ export {};
41
42
  //# sourceMappingURL=meter.d.ts.map
@@ -41,4 +41,5 @@ export declare abstract class SonicPositionGroupElement extends SonicRadioGroupE
41
41
  protected springBack(): void;
42
42
  }
43
43
  //#endregion
44
+ export {};
44
45
  //# sourceMappingURL=position-group.d.ts.map
@@ -9,6 +9,7 @@ export declare abstract class SonicElement extends HTMLElement {
9
9
  protected readonly control: HTMLElement | undefined;
10
10
  protected readonly internals: ElementInternals | undefined;
11
11
  protected readonly sheet: string | undefined;
12
+ protected readonly sheetToken: `--_sonic-${string}` | undefined;
12
13
  blur(): void;
13
14
  connectedCallback(): void;
14
15
  connectedMoveCallback(): void;
@@ -49,11 +49,12 @@ var SonicElement = class SonicElement extends HTMLElement {
49
49
  this.#connection = void 0;
50
50
  throw error;
51
51
  }
52
- const { boxProbe: box, control, sheet } = this;
52
+ const { boxProbe: box, control, sheet, sheetToken: token } = this;
53
53
  if (control && sheet !== void 0) checkStyles(this, {
54
54
  box,
55
55
  control,
56
- sheet
56
+ sheet,
57
+ token
57
58
  });
58
59
  }
59
60
  connectedMoveCallback() {}