@xsynaptic/sonic-ui 0.24.1 → 0.25.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 (104) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/README.md +5 -3
  3. package/dist/default/define/region.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/envelope.js +11 -11
  6. package/dist/default/elements/form-element.js +10 -0
  7. package/dist/default/elements/readout.js +1 -1
  8. package/dist/default/elements/region-layer.js +251 -0
  9. package/dist/default/elements/region.js +157 -0
  10. package/dist/default/elements/sonic-element.js +3 -0
  11. package/dist/default/elements/split.js +2 -2
  12. package/dist/default/elements/toggle.js +2 -2
  13. package/dist/default/elements/value-element.js +7 -3
  14. package/dist/default/elements/value-gestures.js +7 -5
  15. package/dist/default/elements/wave-element.js +12 -3
  16. package/dist/default/elements/waveform.js +38 -2
  17. package/dist/default/elements/wavestrip.js +90 -17
  18. package/dist/default/elements/xy.js +3 -11
  19. package/dist/default/index.js +2 -1
  20. package/dist/default/lib/clock.js +1 -0
  21. package/dist/default/lib/drag-step.js +1 -1
  22. package/dist/default/lib/marker-band.js +40 -29
  23. package/dist/default/lib/region-drag.js +48 -0
  24. package/dist/dev/define/region.d.ts +2 -0
  25. package/dist/dev/define/region.js +8 -0
  26. package/dist/dev/define/region.js.map +1 -0
  27. package/dist/dev/define.d.ts +3 -1
  28. package/dist/dev/define.js +1 -0
  29. package/dist/dev/elements/envelope.js +11 -11
  30. package/dist/dev/elements/envelope.js.map +1 -1
  31. package/dist/dev/elements/form-element.d.ts +1 -0
  32. package/dist/dev/elements/form-element.js +10 -0
  33. package/dist/dev/elements/form-element.js.map +1 -1
  34. package/dist/dev/elements/readout.js +1 -1
  35. package/dist/dev/elements/readout.js.map +1 -1
  36. package/dist/dev/elements/region-layer.js +252 -0
  37. package/dist/dev/elements/region-layer.js.map +1 -0
  38. package/dist/dev/elements/region.d.ts +29 -0
  39. package/dist/dev/elements/region.js +165 -0
  40. package/dist/dev/elements/region.js.map +1 -0
  41. package/dist/dev/elements/sonic-element.d.ts +1 -0
  42. package/dist/dev/elements/sonic-element.js +4 -1
  43. package/dist/dev/elements/sonic-element.js.map +1 -1
  44. package/dist/dev/elements/split.js +2 -2
  45. package/dist/dev/elements/split.js.map +1 -1
  46. package/dist/dev/elements/toggle.js +2 -2
  47. package/dist/dev/elements/toggle.js.map +1 -1
  48. package/dist/dev/elements/value-element.d.ts +3 -1
  49. package/dist/dev/elements/value-element.js +7 -3
  50. package/dist/dev/elements/value-element.js.map +1 -1
  51. package/dist/dev/elements/value-gestures.js +7 -5
  52. package/dist/dev/elements/value-gestures.js.map +1 -1
  53. package/dist/dev/elements/wave-element.d.ts +7 -1
  54. package/dist/dev/elements/wave-element.js +12 -3
  55. package/dist/dev/elements/wave-element.js.map +1 -1
  56. package/dist/dev/elements/waveform.d.ts +7 -0
  57. package/dist/dev/elements/waveform.js +38 -2
  58. package/dist/dev/elements/waveform.js.map +1 -1
  59. package/dist/dev/elements/wavestrip.d.ts +17 -1
  60. package/dist/dev/elements/wavestrip.js +90 -17
  61. package/dist/dev/elements/wavestrip.js.map +1 -1
  62. package/dist/dev/elements/xy.js +3 -11
  63. package/dist/dev/elements/xy.js.map +1 -1
  64. package/dist/dev/index.d.ts +3 -2
  65. package/dist/dev/index.js +2 -1
  66. package/dist/dev/lib/clock.js +1 -0
  67. package/dist/dev/lib/clock.js.map +1 -1
  68. package/dist/dev/lib/drag-step.js +1 -1
  69. package/dist/dev/lib/drag-step.js.map +1 -1
  70. package/dist/dev/lib/marker-band.d.ts +7 -0
  71. package/dist/dev/lib/marker-band.js +40 -29
  72. package/dist/dev/lib/marker-band.js.map +1 -1
  73. package/dist/dev/lib/region-drag.js +49 -0
  74. package/dist/dev/lib/region-drag.js.map +1 -0
  75. package/dist/styles/button.css +12 -4
  76. package/dist/styles/controls.css +1 -0
  77. package/dist/styles/dial.css +35 -18
  78. package/dist/styles/envelope.css +6 -5
  79. package/dist/styles/material/arc.css +2 -2
  80. package/dist/styles/material/cap.css +21 -11
  81. package/dist/styles/material/glass.css +1 -1
  82. package/dist/styles/material/groove.css +3 -3
  83. package/dist/styles/material/keycap.css +2 -2
  84. package/dist/styles/material/lens.css +1 -1
  85. package/dist/styles/material/pane.css +13 -5
  86. package/dist/styles/material/readout.css +4 -3
  87. package/dist/styles/material/{bracket.css → reticle.css} +15 -8
  88. package/dist/styles/material/well.css +2 -2
  89. package/dist/styles/material.css +1 -1
  90. package/dist/styles/meter.css +18 -5
  91. package/dist/styles/number.css +2 -1
  92. package/dist/styles/panel.css +3 -3
  93. package/dist/styles/region.css +88 -0
  94. package/dist/styles/ring.css +2 -1
  95. package/dist/styles/screen.css +7 -5
  96. package/dist/styles/segmented.css +2 -1
  97. package/dist/styles/slider.css +12 -3
  98. package/dist/styles/spectrum.css +9 -1
  99. package/dist/styles/switch.css +3 -2
  100. package/dist/styles/toggle.css +2 -1
  101. package/dist/styles/waveform.css +3 -2
  102. package/dist/styles/wavestrip.css +11 -8
  103. package/dist/styles/xy.css +9 -8
  104. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @xsynaptic/sonic-ui
2
2
 
3
+ ## 0.25.1
4
+
5
+ ### Patch Changes
6
+
7
+ - `formatClock` writes a time that is not finite (a duration not yet known, a live stream's) as an empty string, where it wrote `NaN:NaN`.
8
+
9
+ ## 0.25.0
10
+
11
+ ### Minor Changes
12
+
13
+ - - `<sonic-region>`: an editable region, a child of a wave strip, drawn as a bracket and registered by `./define/region` with `region.css`. A drag of its body or a key moves it and keeps its length. It has `start`, `end`, `kind`, `name` (submitting `name.start` and `name.end`) and `disabled`, speaks its start, carries `:state(dragging)`, and fires `input` and `change`, which bubble through the strip.
14
+ - Region surface: tokens `--sonic-region-bracket`, `--sonic-region-fill`, `--sonic-region-bracket-width` and `--sonic-region-bracket-radius`; hook classes `.sonic-wavestrip-regions`, `.sonic-region` and `.sonic-region-bracket`; `data-sonic-clipped` on a region written past a bound.
15
+ - Wave strip: `formatSpokenRegion(start, end)` words what a region speaks, and `preview` draws a time as a scrub's edge without scrubbing to it.
16
+ - Wave strip and waveform: `valueFromPoint(clientX, clientY)` is the inverse of `clientXOf`.
17
+ - Wave strip, breaking: the slider role, the tab stop and the focus glow are on `.sonic-wavestrip-canvas`, so `.sonic-wavestrip:focus-visible` no longer matches (`:focus-within` does).
18
+ - - XY pad and envelope: the arcs round a held puck or handle are the reticle. Breaking: `.sonic-xy-bracket` is `.sonic-xy-reticle`, `.sonic-envelope-bracket` is `.sonic-envelope-reticle`, and `material/bracket.css` is `material/reticle.css`.
19
+ - - Waveform: `clientXOf(seconds)` is the viewport x where a time was last drawn, as on the wave strip, so a layer over the wave follows a paged window and a drag. The x is not clamped to the window.
20
+ - Wave strip and waveform: `markersFromPoint(clientX, clientY)` lists the markers at a point, nearest first.
21
+ - Wave strip: `renderMarker(marker, element)` draws a point marker; the element lasts until the next `markers` or `renderMarker` write.
22
+ - Waveform: a marker outside the bounds paints its line at the bound.
23
+ - Wave strip: `--sonic-wave` is renamed `--sonic-unplayed`, beside `--sonic-buffered` and `--sonic-scrub`. Breaking for a skin or host that sets it.
24
+
25
+ ### Patch Changes
26
+
27
+ - Older browsers, down to Chrome 111, Safari 16.4 and Firefox 128: the built sheets carry vendor prefixes (`-webkit-mask`, `-webkit-user-select`), shading no longer needs relative colour, a dial's scale and the XY pad's lines no longer need the `cap` unit or `sign()`, and typed entry stays shut without the popover API. With no skin, glass follows `Canvas`: an off-white in light, the engine's dark canvas in dark.
28
+
3
29
  ## 0.24.1
4
30
 
5
31
  ### Patch Changes
@@ -29,8 +55,8 @@
29
55
 
30
56
  - - `--sonic-entry-selection` colours the selected text of a typed entry, which opens with its value selected. Unset, it stays the control's lit colour.
31
57
  - `--sonic-slider-groove-hover-delay` is how long a slider's groove waits under a pointer before it thickens to `--sonic-slider-groove-hover-ratio`. A press or a keyboard focus thickens it at once and leaving thins it at once. Unset, there is no wait.
32
- - `--sonic-readout-depth` scales a readout's cast shadow, inner shine and lighter centre from 0 to 1. Unset, it follows the relief.
33
- - Behaviour change: a readout no longer takes `--sonic-glass-depth` from a screen around it. Inside a screen flattened with `--sonic-glass-depth: 0` its centre follows the relief again, as its shadow always did; set `--sonic-readout-depth: 0` to flatten it.
58
+ - `--sonic-readout-depth` scales a readout's cast shadow, inner shine and lighter center from 0 to 1. Unset, it follows the relief.
59
+ - Behaviour change: a readout no longer takes `--sonic-glass-depth` from a screen around it. Inside a screen flattened with `--sonic-glass-depth: 0` its center follows the relief again, as its shadow always did; set `--sonic-readout-depth: 0` to flatten it.
34
60
 
35
61
  ## 0.21.0
36
62
 
package/README.md CHANGED
@@ -43,6 +43,7 @@ import '@xsynaptic/sonic-ui/define';
43
43
  - The split is the one host with a box: `<sonic-split>` is inline until given a display, and laying it out is yours.
44
44
  - Register the classes under their stock tag names (`sonic-dial`, `sonic-split`). The sheets and the split key on them, so a split under another tag finds no members.
45
45
  - Name every control with `aria-label` or `aria-labelledby` on the host, or with a `<label>` for a form-associated one.
46
+ - Every control that holds a value the user sets is form-associated, as a native input is, and that is deliberate: given a `name` it submits, a form reset returns it to its attributes, a disabled `<fieldset>` disables it, and a `<label>` names it. With no `name` it submits nothing. A value control submits its number and a segmented control its value; a latching button, and a switch or toggle with two positions, submit their `value` only while on, as a checkbox does; an XY pad and a region submit two fields (`name.x` and `name.y`, `name.start` and `name.end`). A momentary button, the displays, the envelope and the split submit nothing.
46
47
  - A boolean attribute is on when present, whatever its value, so `pressed="false"` latches a button. From a framework pass `undefined`, never `false`.
47
48
  - A control uses only the children it documents and adds one child of its own. Children it draws are copied, so a copy has none of your listeners; keep them static. Under a hydrating framework, register the definitions once hydration is done (in React, from an effect).
48
49
  - Events bubble and carry no detail: read the property when one fires. A control knows nothing about playback, so wire its events to your own state and write the property back.
@@ -75,11 +76,12 @@ A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to st
75
76
  | --------------- | ----------------------------------------------- |
76
77
  | `button.css` | core, cap, well, keycap |
77
78
  | `dial.css` | core, cap, glass, readout, value, arc, scale |
78
- | `envelope.css` | core, glass, readout, bracket |
79
+ | `envelope.css` | core, glass, readout, reticle |
79
80
  | `led.css` | core, lens |
80
81
  | `meter.css` | core, lens, groove |
81
82
  | `number.css` | core, cap, value |
82
83
  | `panel.css` | core |
84
+ | `region.css` | core, cap |
83
85
  | `ring.css` | core, arc |
84
86
  | `screen.css` | core, glass, pane, well |
85
87
  | `segmented.css` | core, cap, well, keycap |
@@ -89,8 +91,8 @@ A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to st
89
91
  | `toggle.css` | core, cap, well, keycap |
90
92
  | `waveform.css` | core, cap, glass, readout, value |
91
93
  | `wavestrip.css` | core, cap, glass, readout, value |
92
- | `xy.css` | core, cap, glass, readout, bracket |
94
+ | `xy.css` | core, cap, glass, readout, reticle |
93
95
 
94
96
  An LED inside a control also needs `led.css`, and a button inside a ring needs `button.css`. Only a missing `core.css` or control sheet is warned about, so check a control that draws wrong against this table.
95
97
 
96
- Targets current Chrome and Edge, Safari 26 and Firefox 147. No control mirrors for right-to-left. MIT licensed.
98
+ Tested in current Chrome, Firefox and Safari. Known to work from Chrome 111, Safari 16.4 and Firefox 128; older versions are best effort. No control mirrors for right-to-left. MIT licensed.
@@ -0,0 +1,7 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicRegion } from "../elements/region.js";
3
+
4
+ //#region src/define/region.ts
5
+ defineOnce("sonic-region", SonicRegion);
6
+
7
+ //#endregion
@@ -3,6 +3,7 @@ import "./define/dial.js";
3
3
  import "./define/envelope.js";
4
4
  import "./define/meter.js";
5
5
  import "./define/number.js";
6
+ import "./define/region.js";
6
7
  import "./define/segmented.js";
7
8
  import "./define/slider.js";
8
9
  import "./define/spectrum.js";
@@ -36,7 +36,7 @@ function stageParts(envelope, hook, stages) {
36
36
  }
37
37
  const renderEnvelope = template(`
38
38
  <div class="sonic-envelope" aria-hidden="true">
39
- <div class="sonic-envelope-bracket"></div>
39
+ <div class="sonic-envelope-reticle"></div>
40
40
  <svg class="sonic-envelope-graph" preserveAspectRatio="none" viewBox="0 0 1 1">
41
41
  <path class="sonic-envelope-fill" />
42
42
  <rect class="sonic-envelope-fill sonic-envelope-floor" y="1" />
@@ -123,11 +123,10 @@ var SonicEnvelope = class extends SonicElement {
123
123
  this.reflect("sustain", id);
124
124
  }
125
125
  #bindings = /* @__PURE__ */ new Map();
126
- #envelope = renderEnvelope();
127
- #bracket = requireChild(this.#envelope, ".sonic-envelope-bracket", HTMLDivElement);
128
126
  #claim = new ReadoutClaim(() => {
129
127
  this.#renderReadout();
130
128
  });
129
+ #envelope = renderEnvelope();
131
130
  #curveHandles = stageParts(this.#envelope, "sonic-envelope-curve", curveStages);
132
131
  #fill = requireChild(this.#envelope, ".sonic-envelope-fill", SVGElement);
133
132
  #floor = requireChild(this.#envelope, ".sonic-envelope-floor", SVGElement);
@@ -138,6 +137,7 @@ var SonicEnvelope = class extends SonicElement {
138
137
  #isDriving = false;
139
138
  #line = requireChild(this.#envelope, ".sonic-envelope-line", SVGElement);
140
139
  #readout = new Readout(requireChild(this.#envelope, ".sonic-envelope-readout", HTMLDivElement));
140
+ #reticle = requireChild(this.#envelope, ".sonic-envelope-reticle", HTMLDivElement);
141
141
  #shape = adsrShape({});
142
142
  #waiting = /* @__PURE__ */ new Set();
143
143
  #warned = /* @__PURE__ */ new Set();
@@ -229,15 +229,9 @@ var SonicEnvelope = class extends SonicElement {
229
229
  at: shape.curveHandles.find((point) => point.stage === stage),
230
230
  drives: this.#curveDrives(stage)
231
231
  });
232
- this.#drawBracket();
232
+ this.#drawReticle();
233
233
  this.#renderReadout();
234
234
  }
235
- #drawBracket() {
236
- const part = this.#held;
237
- if (!part) return;
238
- for (const name of ["--_sonic-envelope-x", "--_sonic-envelope-y"]) this.#bracket.style.setProperty(name, part.style.getPropertyValue(name));
239
- writeAttribute(this.#bracket, "data-sonic-part", part.classList.contains("sonic-envelope-curve") ? "curve" : void 0);
240
- }
241
235
  #drawPoint(part, point) {
242
236
  const { at, drives } = point;
243
237
  writeAttribute(part, "hidden", at ? void 0 : "");
@@ -248,6 +242,12 @@ var SonicEnvelope = class extends SonicElement {
248
242
  writeAttribute(part, "data-sonic-axes", drives.map(({ axis }) => axis).join(""));
249
243
  writeAttribute(part, "data-sonic-disabled", isStill ? "" : void 0);
250
244
  }
245
+ #drawReticle() {
246
+ const part = this.#held;
247
+ if (!part) return;
248
+ for (const name of ["--_sonic-envelope-x", "--_sonic-envelope-y"]) this.#reticle.style.setProperty(name, part.style.getPropertyValue(name));
249
+ writeAttribute(this.#reticle, "data-sonic-part", part.classList.contains("sonic-envelope-curve") ? "curve" : void 0);
250
+ }
251
251
  #drives(stage) {
252
252
  const x = this.#bindings.get(stage);
253
253
  const y = stage === "decay" ? this.#bindings.get("sustain") : void 0;
@@ -263,7 +263,7 @@ var SonicEnvelope = class extends SonicElement {
263
263
  const pressed = this.#pressed(event);
264
264
  if (!pressed || pressed.drives.length === 0) return void 0;
265
265
  this.#held = pressed.part;
266
- this.#drawBracket();
266
+ this.#drawReticle();
267
267
  const box = this.#graph.getBoundingClientRect();
268
268
  const travel = {
269
269
  x: Math.max(1, box.width * this.#shape.share),
@@ -47,6 +47,16 @@ var SonicFormElement = class extends SonicElement {
47
47
  if (!internals || this.hasAttribute("aria-label") || this.hasAttribute("aria-labelledby")) return [];
48
48
  return [...internals.labels].filter((label) => label instanceof Element);
49
49
  }
50
+ writeFormFields(fields, state) {
51
+ const { name } = this;
52
+ if (name === "") {
53
+ this.writeFormValue(null, state);
54
+ return;
55
+ }
56
+ const data = new FormData();
57
+ for (const [field, value] of fields) data.set(`${name}.${field}`, String(value));
58
+ this.writeFormValue(data, state);
59
+ }
50
60
  writeFormValue(value, state) {
51
61
  this.internals?.setFormValue(value, `${this.localName}:${state}`);
52
62
  }
@@ -2,7 +2,7 @@ import { requireChild } from "../lib/render.js";
2
2
 
3
3
  //#region src/elements/readout.ts
4
4
  const canPopover = "togglePopover" in HTMLElement.prototype;
5
- const isUnanchored = canPopover && !CSS.supports("anchor-name", "--a");
5
+ const isUnanchored = !CSS.supports("anchor-name", "--a");
6
6
  let readoutCount = 0;
7
7
  var Readout = class {
8
8
  #anchor;
@@ -0,0 +1,251 @@
1
+ import { writeAttribute } from "../lib/write-attribute.js";
2
+ import { placeChildren, template } from "../lib/render.js";
3
+ import { capturePointer } from "../lib/pointer-drag.js";
4
+ import { dragThresholdPx } from "../lib/drag-step.js";
5
+ import { focusByPointer } from "../lib/focus-by-pointer.js";
6
+ import { SonicRegion, linkRegion, ownRegions } from "./region.js";
7
+ import { writeKind } from "../lib/marker-band.js";
8
+ import { moveRegionTo, regionAt, regionTravel, startRegionDrag, stepRegionDrag } from "../lib/region-drag.js";
9
+
10
+ //#region src/elements/region-layer.ts
11
+ const renderPart = template(`
12
+ <div class="sonic-region" role="slider" aria-orientation="horizontal">
13
+ <div class="sonic-region-bracket"></div>
14
+ </div>
15
+ `, HTMLDivElement);
16
+ function hasMoved(from, to) {
17
+ return from.start !== to.start || from.end !== to.end;
18
+ }
19
+ function clippedEnds(region, [low, high]) {
20
+ const ends = [...Math.min(region.start, region.end) < low ? ["start"] : [], ...Math.max(region.start, region.end) > high ? ["end"] : []];
21
+ return ends.length > 0 ? ends.join(" ") : void 0;
22
+ }
23
+ function writeProperty(part, name, value) {
24
+ if (part.style.getPropertyValue(name) !== value) part.style.setProperty(name, value);
25
+ }
26
+ var RegionLayer = class {
27
+ #drawn = [];
28
+ #held;
29
+ #host;
30
+ #key;
31
+ #keyScrub;
32
+ constructor(host, signal) {
33
+ const render = () => {
34
+ this.render();
35
+ };
36
+ const observer = new MutationObserver(render);
37
+ this.#host = host;
38
+ this.#bindPointer(signal);
39
+ this.#bindKeys(signal);
40
+ observer.observe(host.element, { childList: true });
41
+ ownRegions(host.element, (region) => {
42
+ this.#redraw(region);
43
+ }, signal);
44
+ signal.addEventListener("abort", () => {
45
+ observer.disconnect();
46
+ this.#release();
47
+ this.#endKeyScrub();
48
+ }, { once: true });
49
+ }
50
+ abandon() {
51
+ this.#release(true);
52
+ }
53
+ follow() {
54
+ if (this.#stripKey() !== this.#key) this.render();
55
+ }
56
+ isHeld() {
57
+ return this.#held !== void 0;
58
+ }
59
+ render() {
60
+ const host = this.#host;
61
+ const drawn = [...host.element.children].filter((child) => child instanceof SonicRegion).map((region) => this.#draw(region));
62
+ this.#drawn = drawn;
63
+ this.#key = this.#stripKey();
64
+ placeChildren(host.layer, drawn.map(({ part }) => part));
65
+ this.#dropGone();
66
+ }
67
+ take(event) {
68
+ const host = this.#host;
69
+ const { startPx, travelPx } = host.axis();
70
+ const mapping = host.mapping();
71
+ const index = regionAt(this.#drawn.map((entry) => {
72
+ if (!this.#isEnabled(entry)) return;
73
+ const { end, start } = entry.link.span();
74
+ return [mapping.proportionOf(start) * travelPx, mapping.proportionOf(end) * travelPx];
75
+ }), event.clientX - startPx);
76
+ const pressed = index === void 0 ? void 0 : this.#drawn[index];
77
+ if (!pressed) return false;
78
+ this.#endKeyScrub();
79
+ const from = pressed.link.span();
80
+ this.#held = {
81
+ drawn: pressed,
82
+ from,
83
+ outside: host.outside(),
84
+ pointerId: event.pointerId,
85
+ press: event,
86
+ state: startRegionDrag({
87
+ from,
88
+ position: event.clientX,
89
+ thresholdPx: dragThresholdPx(event.pointerType),
90
+ widthPx: travelPx
91
+ })
92
+ };
93
+ pressed.link.hold("pointer");
94
+ capturePointer(host.control, event.pointerId);
95
+ return true;
96
+ }
97
+ #bindKeys(signal) {
98
+ const { layer } = this.#host;
99
+ layer.addEventListener("keydown", (event) => {
100
+ const entry = this.#drawn.find(({ part }) => part === event.target);
101
+ if (!entry || event.defaultPrevented || this.#held) return;
102
+ if (this.#isEnabled(entry)) this.#keyTo(entry, event);
103
+ }, { signal });
104
+ layer.addEventListener("keyup", (event) => {
105
+ if (event.key === "Meta" || event.key === this.#keyScrub?.key) this.#endKeyScrub();
106
+ }, { signal });
107
+ layer.addEventListener("focusin", () => {
108
+ this.render();
109
+ }, { signal });
110
+ layer.addEventListener("focusout", () => {
111
+ this.#endKeyScrub();
112
+ }, { signal });
113
+ }
114
+ #bindPointer(signal) {
115
+ const whileHeld = (type, handle) => {
116
+ this.#host.control.addEventListener(type, (event) => {
117
+ const held = this.#held;
118
+ if (held && event instanceof PointerEvent && held.pointerId === event.pointerId) handle(held, event);
119
+ }, { signal });
120
+ };
121
+ whileHeld("pointermove", (hold, event) => {
122
+ this.#dragTo(hold, event);
123
+ });
124
+ whileHeld("pointerup", (hold) => {
125
+ const isPress = !hold.state.isEngaged;
126
+ this.#release();
127
+ if (isPress) this.#host.seek(hold.press);
128
+ });
129
+ whileHeld("pointercancel", () => {
130
+ this.#release(true);
131
+ });
132
+ whileHeld("lostpointercapture", () => {
133
+ this.#release();
134
+ });
135
+ }
136
+ #dragTo(hold, event) {
137
+ const wasEngaged = hold.state.isEngaged;
138
+ const { span, state } = stepRegionDrag(this.#host.mapping(), hold.state, {
139
+ isFine: event.shiftKey,
140
+ position: event.clientX
141
+ });
142
+ hold.state = state;
143
+ if (!span) return;
144
+ if (!wasEngaged) focusByPointer(hold.drawn.part);
145
+ hold.drawn.link.input(hold.outside?.(event) === true ? hold.from : span);
146
+ }
147
+ #draw(region) {
148
+ const host = this.#host;
149
+ const known = this.#drawn.find((entry) => entry.region === region);
150
+ const entry = known ?? {
151
+ kind: void 0,
152
+ link: linkRegion(region),
153
+ part: renderPart(),
154
+ region
155
+ };
156
+ const { link, part } = entry;
157
+ const mapping = host.mapping();
158
+ const span = link.span();
159
+ const [low, high] = regionTravel(span, mapping.bounds);
160
+ const canMove = this.#isEnabled(entry);
161
+ writeProperty(part, "--_sonic-region-from", String(mapping.proportionOf(span.start)));
162
+ writeProperty(part, "--_sonic-region-to", String(mapping.proportionOf(span.end)));
163
+ if (!known || entry.kind !== region.kind) {
164
+ entry.kind = region.kind;
165
+ writeKind(part, region.kind, "--_sonic-ink");
166
+ }
167
+ writeAttribute(part, "data-sonic-clipped", clippedEnds(region, mapping.bounds));
168
+ writeAttribute(part, "aria-valuemin", String(low));
169
+ writeAttribute(part, "aria-valuemax", String(high));
170
+ writeAttribute(part, "aria-valuenow", String(span.start));
171
+ writeAttribute(part, "aria-valuetext", host.speak(span));
172
+ writeAttribute(part, "aria-disabled", canMove ? void 0 : "true");
173
+ writeAttribute(part, "tabindex", canMove ? "0" : void 0);
174
+ link.draw(part);
175
+ return entry;
176
+ }
177
+ #dropGone() {
178
+ const isGone = (entry) => !this.#drawn.includes(entry) || !this.#isEnabled(entry);
179
+ if (this.#held && isGone(this.#held.drawn)) this.#release();
180
+ if (this.#keyScrub && isGone(this.#keyScrub.drawn)) this.#endKeyScrub();
181
+ }
182
+ #endKeyScrub() {
183
+ const scrub = this.#keyScrub;
184
+ if (!scrub) return;
185
+ const { link } = scrub.drawn;
186
+ this.#keyScrub = void 0;
187
+ link.hold(void 0);
188
+ if (hasMoved(scrub.from, link.span())) link.change();
189
+ }
190
+ #isEnabled({ link }) {
191
+ return !this.#host.isDisabled() && !link.isDisabled();
192
+ }
193
+ #keyTo(entry, event) {
194
+ const mapping = this.#host.mapping();
195
+ const from = entry.link.span();
196
+ const start = mapping.keyTarget(event.key, from.start);
197
+ if (start === void 0) return;
198
+ event.preventDefault();
199
+ const next = moveRegionTo(mapping, from, start);
200
+ if (!event.repeat) {
201
+ this.#endKeyScrub();
202
+ if (entry.link.input(next)) entry.link.change();
203
+ return;
204
+ }
205
+ if (this.#keyScrub?.drawn !== entry || this.#keyScrub.key !== event.key) {
206
+ this.#endKeyScrub();
207
+ this.#keyScrub = {
208
+ drawn: entry,
209
+ from,
210
+ key: event.key
211
+ };
212
+ entry.link.hold("keys");
213
+ }
214
+ entry.link.input(next);
215
+ }
216
+ #redraw(region) {
217
+ if (this.#drawn.every((entry) => entry.region !== region)) {
218
+ this.render();
219
+ return;
220
+ }
221
+ this.#draw(region);
222
+ this.#dropGone();
223
+ }
224
+ #release(isRestored = false) {
225
+ const hold = this.#held;
226
+ if (!hold) return;
227
+ const { link } = hold.drawn;
228
+ this.#held = void 0;
229
+ if (isRestored) link.input(hold.from);
230
+ link.hold(void 0);
231
+ if (hasMoved(hold.from, link.span())) link.change();
232
+ }
233
+ #stripKey() {
234
+ const host = this.#host;
235
+ const mapping = host.mapping();
236
+ const [low, high] = mapping.bounds;
237
+ return [
238
+ low,
239
+ high,
240
+ mapping.proportionOf((low + high) / 2),
241
+ host.isDisabled(),
242
+ host.speak({
243
+ end: high,
244
+ start: high
245
+ })
246
+ ].join("|");
247
+ }
248
+ };
249
+
250
+ //#endregion
251
+ export { RegionLayer };
@@ -0,0 +1,157 @@
1
+ import { SonicFormElement } from "./form-element.js";
2
+
3
+ //#region src/elements/region.ts
4
+ const owners = /* @__PURE__ */ new WeakMap();
5
+ let link;
6
+ function linkRegion(element) {
7
+ return link(element);
8
+ }
9
+ function ownRegions(host, changed, signal) {
10
+ owners.set(host, changed);
11
+ signal.addEventListener("abort", () => {
12
+ if (owners.get(host) === changed) owners.delete(host);
13
+ }, { once: true });
14
+ }
15
+ var SonicRegion = class extends SonicFormElement {
16
+ static observedAttributes = [
17
+ ...SonicFormElement.observedAttributes,
18
+ "end",
19
+ "kind",
20
+ "name",
21
+ "start"
22
+ ];
23
+ static {
24
+ link = (element) => ({
25
+ change: () => {
26
+ element.#report("change");
27
+ },
28
+ draw: (part) => {
29
+ element.#draw(part);
30
+ },
31
+ hold: (by) => {
32
+ element.#held = by;
33
+ element.toggleState("dragging", by === "pointer");
34
+ },
35
+ input: (span) => element.#input(span),
36
+ isDisabled: () => element.isDisabled(),
37
+ span: () => {
38
+ const { end, start } = element.#span;
39
+ return {
40
+ end: Math.max(start, end),
41
+ start: Math.min(start, end)
42
+ };
43
+ }
44
+ });
45
+ }
46
+ get dragging() {
47
+ return this.hasState("dragging");
48
+ }
49
+ get end() {
50
+ return this.#span.end;
51
+ }
52
+ set end(next) {
53
+ if (!this.#held) this.reflect("end", next);
54
+ }
55
+ /** Lowercase letters, digits and hyphens; colours the region through `--sonic-marker-<kind>` */
56
+ get kind() {
57
+ return this.getAttribute("kind") ?? void 0;
58
+ }
59
+ set kind(kind) {
60
+ this.reflect("kind", kind);
61
+ }
62
+ get start() {
63
+ return this.#span.start;
64
+ }
65
+ set start(next) {
66
+ if (!this.#held) this.reflect("start", next);
67
+ }
68
+ #formState = "";
69
+ #held;
70
+ #part;
71
+ #span = {
72
+ end: 0,
73
+ start: 0
74
+ };
75
+ attributeChangedCallback(name) {
76
+ const isSpan = name === "end" || name === "start";
77
+ if (isSpan && this.#held) return;
78
+ if (isSpan) this.#readSpan(name);
79
+ this.render();
80
+ }
81
+ connectedCallback() {
82
+ this.upgradeProperties("end", "kind", "start");
83
+ super.connectedCallback();
84
+ }
85
+ formResetCallback() {
86
+ if (this.#held) return;
87
+ this.#readSpan("start");
88
+ this.#readSpan("end");
89
+ this.render();
90
+ }
91
+ connect() {
92
+ this.render();
93
+ if (this.parentElement?.localName !== "sonic-wavestrip");
94
+ }
95
+ focusTarget() {
96
+ return this.#part;
97
+ }
98
+ render() {
99
+ if (!this.isBound() || !this.parentElement) return;
100
+ owners.get(this.parentElement)?.(this);
101
+ }
102
+ restoreState(state) {
103
+ const [start = NaN, end = NaN] = state.split(",").map(Number);
104
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return;
105
+ this.#span = {
106
+ end,
107
+ start
108
+ };
109
+ this.render();
110
+ }
111
+ #draw(part) {
112
+ this.#part = part;
113
+ this.forwardNaming(part, true);
114
+ this.#writeForm();
115
+ }
116
+ #input(span) {
117
+ const hasMoved = span.start !== this.#span.start || span.end !== this.#span.end;
118
+ if (hasMoved) {
119
+ this.#span = span;
120
+ this.render();
121
+ this.#report("input");
122
+ }
123
+ return hasMoved;
124
+ }
125
+ #readSpan(name) {
126
+ const { end, start } = this.#span;
127
+ if (name === "end") {
128
+ this.#span = {
129
+ end: this.numberAttribute("end", start),
130
+ start
131
+ };
132
+ return;
133
+ }
134
+ const next = this.numberAttribute("start", 0);
135
+ this.#span = {
136
+ end: this.hasAttribute("end") ? end : next,
137
+ start: next
138
+ };
139
+ }
140
+ #report(type) {
141
+ this.dispatchEvent(new Event(type, {
142
+ bubbles: true,
143
+ composed: type === "input"
144
+ }));
145
+ }
146
+ #writeForm() {
147
+ const { end, start } = this.#span;
148
+ const state = `${String(start)},${String(end)}`;
149
+ const formState = `${this.name}=${state}`;
150
+ if (formState === this.#formState) return;
151
+ this.#formState = formState;
152
+ this.writeFormFields([["start", start], ["end", end]], state);
153
+ }
154
+ };
155
+
156
+ //#endregion
157
+ export { SonicRegion, linkRegion, ownRegions };
@@ -135,6 +135,9 @@ var SonicElement = class SonicElement extends HTMLElement {
135
135
  this[name] = value;
136
136
  }
137
137
  }
138
+ usesChild(_child) {
139
+ return false;
140
+ }
138
141
  #checkBox(control, checked, box) {}
139
142
  #checkHost(checked) {}
140
143
  #focused() {
@@ -8,7 +8,7 @@ const modeNames = /* @__PURE__ */ new Set([
8
8
  "equal",
9
9
  "proportional"
10
10
  ]);
11
- const gridSlop = 1e-9;
11
+ const gridTolerance = 1e-9;
12
12
  const lockAttributes = ["data-sonic-locked", "disabled"];
13
13
  function isMode(name) {
14
14
  return name !== null && modeNames.has(name);
@@ -19,7 +19,7 @@ function isLocked(member) {
19
19
  function gridLimits(mapping, [low, high]) {
20
20
  const bottom = mapping.snap(low);
21
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];
22
+ return [bottom < low - gridTolerance ? mapping.keyTarget("ArrowRight", bottom) ?? bottom : bottom, top > high + gridTolerance ? mapping.keyTarget("ArrowLeft", top) ?? top : top];
23
23
  }
24
24
  function modelOf(member, value, isFree) {
25
25
  const mapping = member.link.model.mapping();
@@ -19,7 +19,7 @@ const renderPosition = template(`
19
19
  </button>
20
20
  `, HTMLButtonElement);
21
21
  const renderBare = template(`<button class="sonic-toggle-position" role="switch" type="button"></button>`, HTMLButtonElement);
22
- const pressSlop = 2;
22
+ const pressTolerance = 2;
23
23
  var SonicToggle = class extends SonicPositionGroupElement {
24
24
  bare = renderBare();
25
25
  defaultOrientation = "horizontal";
@@ -72,7 +72,7 @@ var SonicToggle = class extends SonicPositionGroupElement {
72
72
  if (!drag.hasMoved && moved < drag.thresholdPx) return;
73
73
  drag.hasMoved = true;
74
74
  this.toggleState("dragging", true);
75
- if (moved >= pressSlop * drag.thresholdPx) drag.isPress = false;
75
+ if (moved >= pressTolerance * drag.thresholdPx) drag.isPress = false;
76
76
  if (this.#slideTo(positionAt(drag.travel, pointer), event.pointerId)) drag.isPress = false;
77
77
  },
78
78
  release: (drag) => {