@xsynaptic/sonic-ui 0.20.0 → 0.22.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 (57) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +22 -108
  3. package/dist/default/elements/button.js +2 -1
  4. package/dist/default/elements/envelope.js +3 -6
  5. package/dist/default/elements/field-gesture.js +3 -0
  6. package/dist/default/elements/split.js +6 -7
  7. package/dist/default/elements/switch.js +5 -3
  8. package/dist/default/elements/toggle.js +10 -5
  9. package/dist/default/elements/value-element.js +3 -0
  10. package/dist/default/elements/value-entry.js +5 -18
  11. package/dist/default/elements/waveform.js +15 -4
  12. package/dist/default/elements/xy.js +33 -5
  13. package/dist/default/lib/double-press.js +30 -0
  14. package/dist/dev/elements/button.js +2 -1
  15. package/dist/dev/elements/button.js.map +1 -1
  16. package/dist/dev/elements/envelope.js +3 -6
  17. package/dist/dev/elements/envelope.js.map +1 -1
  18. package/dist/dev/elements/field-gesture.js +3 -0
  19. package/dist/dev/elements/field-gesture.js.map +1 -1
  20. package/dist/dev/elements/number.js +1 -4
  21. package/dist/dev/elements/number.js.map +1 -1
  22. package/dist/dev/elements/split.js +6 -7
  23. package/dist/dev/elements/split.js.map +1 -1
  24. package/dist/dev/elements/switch.js +5 -3
  25. package/dist/dev/elements/switch.js.map +1 -1
  26. package/dist/dev/elements/toggle.js +10 -5
  27. package/dist/dev/elements/toggle.js.map +1 -1
  28. package/dist/dev/elements/value-element.js +3 -0
  29. package/dist/dev/elements/value-element.js.map +1 -1
  30. package/dist/dev/elements/value-entry.js +5 -18
  31. package/dist/dev/elements/value-entry.js.map +1 -1
  32. package/dist/dev/elements/waveform.d.ts +2 -0
  33. package/dist/dev/elements/waveform.js +15 -4
  34. package/dist/dev/elements/waveform.js.map +1 -1
  35. package/dist/dev/elements/xy.d.ts +4 -0
  36. package/dist/dev/elements/xy.js +33 -5
  37. package/dist/dev/elements/xy.js.map +1 -1
  38. package/dist/dev/lib/double-press.js +31 -0
  39. package/dist/dev/lib/double-press.js.map +1 -0
  40. package/dist/styles/button.css +1 -0
  41. package/dist/styles/dial.css +1 -1
  42. package/dist/styles/envelope.css +2 -1
  43. package/dist/styles/led.css +7 -19
  44. package/dist/styles/material/cap.css +26 -7
  45. package/dist/styles/material/core.css +9 -1
  46. package/dist/styles/material/glass.css +1 -2
  47. package/dist/styles/material/lens.css +18 -1
  48. package/dist/styles/material/pane.css +2 -5
  49. package/dist/styles/material/readout.css +11 -3
  50. package/dist/styles/material/value.css +1 -1
  51. package/dist/styles/material/well.css +8 -1
  52. package/dist/styles/meter.css +6 -4
  53. package/dist/styles/number.css +8 -9
  54. package/dist/styles/slider.css +29 -11
  55. package/dist/styles/toggle.css +4 -0
  56. package/dist/styles/xy.css +4 -19
  57. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @xsynaptic/sonic-ui
2
2
 
3
+ ## 0.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - - `--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.
8
+ - `--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.
9
+ - `--sonic-readout-depth` scales a readout's cast shadow, inner shine and lighter centre from 0 to 1. Unset, it follows the relief.
10
+ - 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.
11
+
12
+ ## 0.21.0
13
+
14
+ ### Minor Changes
15
+
16
+ - - Breaking: the number box is plain: its digits alone in the text colour, with no glass, edge or corner. Wrap it in `.sonic-screen` for the box (`--sonic-screen-size` set to the number's size and `--sonic-screen-inset-ratio: 0` gives the old one). `--sonic-number-radius-ratio` and `--sonic-number-inset-ratio` are removed, `--sonic-radius` and the glass tokens no longer reach it, and `number.css` needs only the core, cap and value material sheets.
17
+ - With no skin a meter no longer draws a near-black bar: its bed is clear and its unlit segments are a faint tint of the text colour, like the LED. Skins are unchanged.
18
+ - A screen now shrinks in a flex row; give one that holds a text readout a `min-inline-size`.
19
+ - A button sets `user-select: none`, as every other control does, so a text legend no longer selects under a long press on touch.
20
+ - A held XY puck stays solid, as a held envelope handle does; the hold shows as the glow, or as the bracket on a skin with no relief. It no longer turns hollow.
21
+ - `--sonic-focus-radius` rounds the focus outline and glow of the plain boxes (number box, waveform, wave strip), which have no corner of their own. Unset, they stay square.
22
+ - - With no skin, every unlit part follows the text colour at 30%, as the LED and the meter did: a dial's ring, a slider's groove, the ring, the pad's lines, the wave strip. Glass, a disabled envelope handle and a disabled puck stay solid. Skins are unchanged.
23
+ - Tokens: `--sonic-touch-action` reaches the dial and the number box, so one set on a wrapper for a slider now reaches dials inside it. `--sonic-radius` reaches every readout; `--sonic-readout-radius-ratio` is new and wins when set. `--sonic-focus-radius` also rounds the focus of a slider and a switch position.
24
+ - Focus: a visible `--sonic-focus-outline` is drawn on the cap of a button, a segmented option and a dial, where the cap covered it.
25
+ - Waveform: a repeating key scrubs, as on the wave strip: `input` on each repeat and one `change` on keyup.
26
+ - Split: `disabled` freezes the members against gestures; a written `value` still lands.
27
+ - XY pad: `double-press="reset"` (default `none`), `:state(revealed)`, `dragging` and `revealed` getters, and `sonic-reveal`. A form reset during a drag is dropped.
28
+ - Toggle: a press that slides a few pixels is still a press. A coloured lit toggle takes system colours in forced colours.
29
+ - Button and switch: a key prevented in a capture listener no longer holds a momentary button, a bat press let go off the switch is cancelled, and an LED in a disabled latched button or option dims where it stayed lit.
30
+ - Screen: its text can be selected (a screen that is itself a button sets its own `user-select`), and its `--sonic-glass-depth` no longer flattens the well of a button inside it.
31
+ - The keycap sheen follows `--sonic-light-tilt`.
32
+
3
33
  ## 0.20.0
4
34
 
5
35
  ### Minor Changes
package/README.md CHANGED
@@ -2,9 +2,7 @@
2
2
 
3
3
  Native custom elements for audio interfaces. Each control renders in the light DOM, skinned with `--sonic-*` tokens (CSS custom properties). No framework, no runtime dependencies.
4
4
 
5
- Early and unstable: any 0.x release may break the API.
6
-
7
- This README holds what the package's own files do not say. For the rest, read the package:
5
+ Early and unstable: any 0.x release may break the API, and the changelog is where a control's behaviour is described. This README holds setup and the contracts every control shares. For the rest, read the package:
8
6
 
9
7
  - Properties and methods: `dist/dev/elements/<control>.d.ts`. Every attribute has a property of the same name in camel case.
10
8
  - Tokens and their defaults: `dist/styles/<control>.css` and `dist/styles/material/`, where each token is read as `var(--sonic-…, default)`.
@@ -23,6 +21,7 @@ import '@xsynaptic/sonic-ui/define';
23
21
  ```css
24
22
  @layer base, sonic, components;
25
23
 
24
+ @import 'reset.css' layer(base);
26
25
  @import '@xsynaptic/sonic-ui/controls.css';
27
26
  @import '@xsynaptic/sonic-ui/skins/slate.css';
28
27
  ```
@@ -33,117 +32,32 @@ import '@xsynaptic/sonic-ui/define';
33
32
  </body>
34
33
  ```
35
34
 
36
- - Every sheet sits in the `sonic` layer, so declare it below the layer your own rules live in.
37
- - A rule outside any layer beats every layered one, so an unlayered reset (`* { margin: 0; padding: 0 }`, a zeroed `border`, a bare `button`) strips padding, borders, type sizes and colours from the controls. Import a reset you do not own into a layer declared before `sonic`:
38
-
39
- ```css
40
- @layer base, sonic, components;
41
-
42
- @import 'reset.css' layer(base);
43
- ```
35
+ - Every sheet sits in the `sonic` layer. A rule outside any layer beats every layered one, so an unlayered reset strips padding, borders and type from the controls: import a reset into a layer declared before `sonic`, and keep your own rules in one declared after.
36
+ - A skin sheet does nothing until its class, `sonic-skin-<name>`, is on an ancestor. The skins are `slate`, `amber`, `ivory`, `lime` and `flat`. With no skin a control is bare, drawn in the text colour around it.
37
+ - `./define` registers every control and `./define/<control>` registers one. `.` exports the classes and registers nothing.
38
+ - Under a bundler that sets the `development` condition, as Vite's dev server does, the controls warn in the console about a missing sheet, a reset, a box property on a host and a child they do not use. Read the console before debugging.
44
39
 
45
- - A skin sheet does nothing until its class, `sonic-skin-<name>`, is on an ancestor of the controls. The skins are `slate`, `amber`, `ivory`, `lime` and `flat`; a wrapper with another skin's class reskins what is inside it. With no skin a control is bare: no cap and no well, drawn in the text colour around it.
46
- - `./define` registers every control. `./define/<control>` registers one, and `.` exports the classes and registers nothing.
47
- - Under a bundler that sets the `development` condition, as Vite's dev server does, the controls warn in the console about a control's own sheet or `material/core.css` missing, a padding lost to an unlayered rule, a box property set on a host, a child they do not use and a binding that found nothing. Read the console before debugging.
40
+ ## Contracts
48
41
 
49
- ## What every control shares
50
-
51
- - The host is `display: contents` and the control is an element inside it. A width, margin or transform on the host does nothing: size a control with its token (`--sonic-dial-size`, `--sonic-slider-length`), which can be set on the host or any ancestor, and place it with its parent's layout.
52
- - Name every control with `aria-label` or `aria-labelledby` on the host, or with a `<label>` for a form-associated one; the control moves the name onto its focusable part.
42
+ - The host is `display: contents` and the control is an element inside it. A width, margin or transform on the host does nothing: size a control with its token (`--sonic-dial-size`, `--sonic-slider-length`), set on the host or any ancestor, and place it with its parent's layout.
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
+ - 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
+ - Name every control with `aria-label` or `aria-labelledby` on the host, or with a `<label>` for a form-associated one.
53
46
  - A boolean attribute is on when present, whatever its value, so `pressed="false"` latches a button. From a framework pass `undefined`, never `false`.
54
- - A control uses only the children it documents below, and adds one child of its own. Under a hydrating framework, register the definitions once hydration is done (in React, from an effect), so the added child is no mismatch.
55
- - Events bubble and carry no detail: read the property when one fires. A control knows nothing about playback, so wire `input` and `change` to your own state and write `value` back.
56
- - Buttons, value controls, the segmented control, the switch, the toggle and the XY pad are form-associated: give one a `name` and it submits, resets and restores with its form.
57
- - States a control sets itself are custom states on the host, selected with `:state()`: `dragging`, `cancelling`, `editing`, `revealed`, `springing`, `modulated`, `pressed`, `held`, `ladder`, `empty` and `pending`.
58
-
59
- ## Value controls
60
-
61
- The dial, the slider, the number box and both wave controls hold one number, from `min` (0) to `max` (100) in steps of `step` (1).
62
-
63
- - `input` fires as the value moves and `change` once when a gesture ends, however it ends.
64
- - A drag owns the value: a write to `value` during one is dropped, so a seek bar fed by `timeupdate` needs no guard of its own.
65
- - A double press, or Enter, opens typed entry. `double-press="reset"` makes it go back to `default` instead. Cmd-click on Apple platforms, Ctrl-click elsewhere, and Delete reset whichever is chosen, once `default` is set.
66
- - `formatValue` and `parseValue` are properties holding functions. What the first writes, the second has to read, or typed entry opens on text it cannot take; `formatEntry` covers the case where it cannot.
67
- - The readout bubble is opt-in, with `readout`. Assistive technology hears the value without it; `formatSpokenValue` words it.
68
- - `taper="log"` gives even octaves on a range above zero, and `midpoint` bends a range that starts at zero. On a tapered control the arrow keys move by travel; `key-step` makes them move by value.
69
- - `positions` is a property holding the numbers a stepped control stops at, spaced evenly whatever their values; their labels come from `formatValue`.
70
- - `modulationValue` is a property only, meant to be written every frame. It moves the light and fires nothing.
71
- - To take a key from a control, call `preventDefault()` in a capture listener.
72
-
73
- ## Press controls
74
-
75
- A button's children are its legend, an icon or a label, always an element. They are copied into the cap, so the copy has none of your listeners and any id in it is prefixed; keep them static. A child with `data-sonic-when="pressed"` or `"released"` shows only in that state.
76
-
77
- ```html
78
- <sonic-button aria-label="Loop" latching>
79
- <span class="sonic-led"></span>
80
- <svg aria-hidden="true">…</svg>
81
- </sonic-button>
82
- ```
83
-
84
- - A plain button fires `click`. `latching` makes it stay pressed, with `pressed` and a `change` event; `momentary` makes it pressed for as long as it is held, and acts on the press, not the release.
85
- - `soft-disabled` stops presses but keeps the tab stop, for a button that would otherwise lose focus when it disables itself. `busy` leaves it pressable.
86
- - The segmented control, the switch and the toggle take one child per position, each with `data-sonic-value`, and hold the chosen one in `value`. They fire `change` only. A switch or a toggle with no children is on or off, held in `checked`.
87
- - On a position child, `data-sonic-disabled` locks it, `data-sonic-momentary` makes an end position spring back, and on a toggle `data-sonic-lit` lights the well there (`ok`, `warning` or `danger` for the colour). A lock stops the user, not your code: a `value` written to a disabled position is chosen.
88
-
89
- ```html
90
- <sonic-segmented aria-label="Filter" value="lp">
91
- <span data-sonic-value="lp">LP</span>
92
- <span data-sonic-value="bp">BP</span>
93
- <span data-sonic-value="hp">HP</span>
94
- </sonic-segmented>
95
- ```
96
-
97
- ## Parts that are only a class
98
-
99
- The LED, the panel, the ring and the screen have no behaviour, so they are classes on your own elements and need no registration.
100
-
101
- - `<span class="sonic-led">` is unlit until it carries `data-sonic-lit`, whose value picks the colour: empty, `idle`, `ok`, `warning` or `danger`. Inside a latching button or a toggle's cap it lights with the control.
102
- - `class="sonic-ring"` draws a lit arc around its one child, from `--sonic-ring-from` to `--sonic-ring-to`, each 0 to 1. Give it your own `role="progressbar"`.
103
- - `class="sonic-screen"` is a box of glass that holds your own text, a canvas or a control, and is sized like a `div`: it fills a block, hugs in a flex row, and is never shorter than `--sonic-screen-size`. A wave strip, waveform, spectrum, envelope or XY pad draws no glass of its own, so wrap one in a screen for the encased look; its padding is the inset, and it centres what it holds, so a control that should span it is given its length (`--sonic-wavestrip-length: 100%`). A control with `fill` needs the screen given a height.
104
- - `class="sonic-panel"` groups controls under a `sonic-panel-title`.
105
-
106
- ## Displays
107
-
108
- A display takes its signal as data and never opens an `AudioContext`.
109
-
110
- - `<sonic-meter>`: write `level`, a linear peak amplitude, as often as you have one; the meter converts to decibels and applies its `ballistics`. Write `value` instead to show a number in the meter's own units exactly as given. `segments` turns the bar into a ladder. It fires nothing and has no role: read `peak`, call `resetPeak()`, and announce numbers yourself.
111
- - `<sonic-spectrum>`: set `analyser` to an `AnalyserNode`, or call `push(decibels)` with a `Float32Array` holding one level per bin and set `sample-rate`.
112
-
113
- ## Controls that drive other controls
114
-
115
- - `<sonic-split>` wraps value controls and keeps their values adding up to `total`. `mode` chooses who gives way (`proportional`, `equal` or `cascade`), and `data-sonic-locked` on a member holds it still. The others move a microtask after the one the user moved, so its `input` arrives first.
116
- - `<sonic-envelope>` has no value and no events. Each of its attributes (`delay`, `attack`, `hold`, `decay`, `sustain`, `release`, and `attack-curve`, `decay-curve`, `release-curve`) names the id of a dial, a slider or a number box; dragging a handle turns that control, which fires its own `input`. Stages share the width equally, whatever their times.
117
- - `<sonic-xy>` holds `x` and `y`, each with its own `x-min`, `x-max`, `x-step` and so on, and submits them as `name.x` and `name.y`.
118
- - `envelopeCurve` and `crossfadeGains` are exported so your DSP computes what the control draws.
119
-
120
- ## Wave controls
121
-
122
- `<sonic-wavestrip>` is the overview of a track, scrubbed to seek; `<sonic-waveform>` is the close-up that scrolls under a fixed playhead. Both are value controls in seconds that take their data as properties (`peaks`, `markers`); the types list the rest. `peaks` is `{ pairsPerSecond, samples }`, and the full scale follows the array unless `fullScale` is set.
123
-
124
- A wave control shows and reads a clock (`1:23`, typed or drawn) and speaks a duration in the language around it, the nearest `lang`. Setting `formatValue`, `parseValue` or `formatSpokenValue` replaces that part; `formatClock` and `parseClock` are the same pair, exported so your own time text matches.
125
-
126
- Their events bubble and carry no detail, so read the property when one fires: `revealed` on `sonic-reveal`, `currentMarker` on `sonic-marker`, and the strip's `hoverValue` on `sonic-hover`, which `clientXOf(value)` turns into a viewport x.
127
-
128
- The strip draws one part per marker inside `.sonic-wavestrip-markers`, in `start` order: a `.sonic-wavestrip-marker` dot, or a `.sonic-wavestrip-region` when the marker has an `end`. Marker labels are hidden from assistive technology; fold `currentMarker` into `formatSpokenValue` to have one spoken.
129
-
130
- The waveform parks the current marker's label in its corner and fades it after the next marker crosses the playhead, never before: `--sonic-waveform-label-fade-start` (0) and `--sonic-waveform-label-fade-end` (1) run from that crossing, 0, to the next label reaching the park, 1.
131
-
132
- The waveform calls `requestPeaks(fromSeconds, toSeconds)` as its window moves, often every frame, so return at once for a span already loaded or on its way. Return a promise, or several, and the waveform repaints as each settles; an `async` function is the wrong shape, since it hands back a new settled promise each call. `pending` is yours to keep: list the spans being fetched, and clear each as it lands.
133
-
134
- With `zoomable`, a ctrl or cmd wheel, a pinch and the `+` and `-` keys set the waveform's `zoom` and fire `sonic-zoom`; a write to `zoom` fires nothing.
135
-
136
- With `fill` a control takes its container's height and its ratios follow. `--sonic-wavestrip-marker-size`, `--sonic-wavestrip-groove-size` and `--sonic-waveform-label-font-size` hold an absolute length instead.
47
+ - 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
+ - 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.
49
+ - A value control fires `input` as its value moves and `change` once when a gesture ends. A drag owns the value: a write to `value` during one is dropped.
50
+ - `formatValue` and `parseValue` are properties holding functions. Once `parseValue` is set it has to read what `formatValue` writes, or typed entry opens on text it cannot take.
51
+ - A display takes its signal as data, and nothing in the library fetches, decodes or opens an `AudioContext`.
52
+ - States a control sets itself are custom states on the host, selected with `:state()`.
53
+ - The LED, the panel, the ring and the screen have no behaviour, so they are classes on your own elements (`sonic-led`, `sonic-panel`, `sonic-ring`, `sonic-screen`). A control draws no glass of its own: wrap one in a screen for the encased look.
137
54
 
138
55
  ## Skinning
139
56
 
140
- A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to start one. Tokens, hook classes (`.sonic-dial`, `.sonic-wavestrip-marker`), `data-sonic-*` attributes and states are the public surface. Anything starting `--_sonic-` is private and changes without notice.
57
+ A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to start one. Tokens, hook classes (`.sonic-dial`), `data-sonic-*` attributes and states are the public surface. Anything starting `--_sonic-` is private and changes without notice.
141
58
 
142
59
  - Set tokens, not rules on hook classes. If a look needs a hook-class rule, a token is missing.
143
- - Most geometry tokens end in `-ratio` and are a unitless share of the control's size, so one size token scales the whole control.
144
- - A length token takes any CSS length except a percentage. For a fluid size use container units.
145
- - `--sonic-relief` runs from 0, flat, to 1, and scales every bevel and shadow at once. `--sonic-light-tilt` turns the one light they all follow.
146
- - `--sonic-target-size` widens the region that takes a press past the control's box. It is unset by default.
60
+ - A token ending in `-ratio` is a unitless share of the control's size, so one size token scales the whole control. A length token takes any CSS length except a percentage.
147
61
 
148
62
  ## Loading less
149
63
 
@@ -163,7 +77,7 @@ A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to st
163
77
  | `envelope.css` | core, glass, readout, bracket |
164
78
  | `led.css` | core, lens |
165
79
  | `meter.css` | core, lens, groove |
166
- | `number.css` | core, cap, glass, pane, well, value |
80
+ | `number.css` | core, cap, value |
167
81
  | `panel.css` | core |
168
82
  | `ring.css` | core, arc |
169
83
  | `screen.css` | core, glass, pane, well |
@@ -176,6 +90,6 @@ A skin is one rule that sets tokens on a class; copy `dist/skins/flat.css` to st
176
90
  | `wavestrip.css` | core, cap, glass, readout, value |
177
91
  | `xy.css` | core, cap, glass, readout, bracket |
178
92
 
179
- An LED inside a control also needs `led.css`, and a button inside a ring needs `button.css`. No warning names a missing material sheet other than `core.css`, so check a control that draws wrong against this table.
93
+ 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.
180
94
 
181
95
  Targets current Chrome and Edge, Safari 26 and Firefox 147. No control mirrors for right-to-left. MIT licensed.
@@ -167,7 +167,8 @@ var SonicButton = class extends SonicFormElement {
167
167
  holding.hold(event.pointerId);
168
168
  }, { signal });
169
169
  button.addEventListener("keydown", (event) => {
170
- if (event.repeat || !holdKeys.has(event.key) || !this.#isMomentary()) return;
170
+ if (event.repeat || event.defaultPrevented) return;
171
+ if (!holdKeys.has(event.key) || !this.#isMomentary()) return;
171
172
  holding.hold(event.key);
172
173
  }, { signal });
173
174
  }
@@ -139,11 +139,10 @@ var SonicEnvelope = class extends SonicElement {
139
139
  #line = requireChild(this.#envelope, ".sonic-envelope-line", SVGElement);
140
140
  #readout = new Readout(requireChild(this.#envelope, ".sonic-envelope-readout", HTMLDivElement));
141
141
  #shape = adsrShape({});
142
- #signal;
143
142
  #waiting = /* @__PURE__ */ new Set();
144
143
  #warned = /* @__PURE__ */ new Set();
145
144
  attributeChangedCallback(name) {
146
- if (!this.#signal) return;
145
+ if (!this.isBound()) return;
147
146
  if (name === "disabled") {
148
147
  this.#gesture?.end();
149
148
  this.#draw();
@@ -158,12 +157,10 @@ var SonicEnvelope = class extends SonicElement {
158
157
  }
159
158
  connect(signal) {
160
159
  const envelope = this.#envelope;
161
- this.#signal = signal;
162
160
  this.keepControl(envelope, signal);
163
161
  this.#bind();
164
162
  this.#bindPointer(envelope, signal);
165
163
  signal.addEventListener("abort", () => {
166
- this.#signal = void 0;
167
164
  this.#unbind();
168
165
  }, { once: true });
169
166
  }
@@ -201,7 +198,7 @@ var SonicEnvelope = class extends SonicElement {
201
198
  release: ({ drives }, moved) => {
202
199
  for (const { axis, binding } of drives) {
203
200
  if (!moved.includes(axis)) continue;
204
- binding.element.dispatchEvent(new Event("change", { bubbles: true }));
201
+ binding.link.change();
205
202
  }
206
203
  },
207
204
  toggle: (isDragging) => {
@@ -308,7 +305,7 @@ var SonicEnvelope = class extends SonicElement {
308
305
  this.#waiting.add(key);
309
306
  arrives(() => {
310
307
  this.#waiting.delete(key);
311
- if (this.#signal) this.#bind();
308
+ if (this.isBound()) this.#bind();
312
309
  });
313
310
  }
314
311
  #renderReadout() {
@@ -53,6 +53,9 @@ function bindFieldGesture(target, options, signal) {
53
53
  })
54
54
  };
55
55
  },
56
+ lift: ({ hold }, event) => {
57
+ options.lift?.(hold, event);
58
+ },
56
59
  move: (held, event) => {
57
60
  const { axes } = held.hold;
58
61
  const step = stepFieldDrag({
@@ -58,12 +58,12 @@ var SonicSplit = class extends SonicElement {
58
58
  #isWarned = false;
59
59
  #isWriting = false;
60
60
  #members = [];
61
- #signal;
62
61
  #snapshot;
63
62
  #total = 0;
64
63
  #waiting = /* @__PURE__ */ new Set();
65
64
  attributeChangedCallback(name) {
66
- if ((name === "mode" || name === "total") && this.#signal) this.#bind();
65
+ if ((name === "mode" || name === "total") && this.isBound()) this.#bind();
66
+ if (name === "disabled" && this.isBound()) this.#limit();
67
67
  }
68
68
  connectedCallback() {
69
69
  this.upgradeProperties("mode", "total");
@@ -73,7 +73,6 @@ var SonicSplit = class extends SonicElement {
73
73
  const observer = new MutationObserver((records) => {
74
74
  if (records.some((record) => record.attributeName !== null && lockAttributes.includes(record.attributeName)) || !this.#isSameMembers(this.#find())) this.#bind();
75
75
  });
76
- this.#signal = signal;
77
76
  if (this.internals) this.internals.role = "group";
78
77
  observer.observe(this, {
79
78
  attributeFilter: lockAttributes,
@@ -86,7 +85,6 @@ var SonicSplit = class extends SonicElement {
86
85
  }, { signal });
87
86
  signal.addEventListener("abort", () => {
88
87
  observer.disconnect();
89
- this.#signal = void 0;
90
88
  this.#isTotalKept = false;
91
89
  this.#unbind();
92
90
  }, { once: true });
@@ -152,11 +150,12 @@ var SonicSplit = class extends SonicElement {
152
150
  })[index] ?? target;
153
151
  }
154
152
  #limit() {
153
+ const isFrozen = this.isDisabled();
155
154
  const values = this.#members.map(({ seen }) => seen);
156
155
  for (const [index, member] of this.#members.entries()) {
157
156
  const model = this.#model(values, member);
158
157
  const value = values[index] ?? member.seen;
159
- member.link.model.setLimit(isLocked(member) ? [value, value] : gridLimits(member.link.model.mapping(), splitLimits(model, index, this.#total)));
158
+ member.link.model.setLimit(isFrozen || isLocked(member) ? [value, value] : gridLimits(member.link.model.mapping(), splitLimits(model, index, this.#total)));
160
159
  }
161
160
  }
162
161
  #model(values, mover) {
@@ -192,7 +191,7 @@ var SonicSplit = class extends SonicElement {
192
191
  queueMicrotask(() => {
193
192
  this.#isWriting = true;
194
193
  try {
195
- for (const { element } of changed) element.dispatchEvent(new Event("change", { bubbles: true }));
194
+ for (const { link } of changed) link.change();
196
195
  } finally {
197
196
  this.#isWriting = false;
198
197
  }
@@ -226,7 +225,7 @@ var SonicSplit = class extends SonicElement {
226
225
  this.#waiting.add(tag);
227
226
  customElements.whenDefined(tag).then(() => {
228
227
  this.#waiting.delete(tag);
229
- if (this.#signal) this.#bind();
228
+ if (this.isBound()) this.#bind();
230
229
  });
231
230
  }
232
231
  #warnReach() {}
@@ -1,5 +1,5 @@
1
1
  import { requireChild, template } from "../lib/render.js";
2
- import { bindDrag } from "../lib/pointer-drag.js";
2
+ import { bindDrag, capturePointer } from "../lib/pointer-drag.js";
3
3
  import { soleStep } from "../lib/positions.js";
4
4
  import { isUnder } from "./radio-group.js";
5
5
  import { SonicPositionGroupElement } from "./position-group.js";
@@ -28,7 +28,7 @@ var SonicSwitch = class extends SonicPositionGroupElement {
28
28
  if (this.#isOnBat(event)) return true;
29
29
  const target = this.pressTarget(index);
30
30
  if (this.isOptionDisabled(target) || !this.isMomentary(target)) return false;
31
- this.group.setPointerCapture(event.pointerId);
31
+ capturePointer(this.group, event.pointerId);
32
32
  this.hold(target, event.pointerId);
33
33
  return true;
34
34
  }
@@ -68,8 +68,10 @@ var SonicSwitch = class extends SonicPositionGroupElement {
68
68
  };
69
69
  },
70
70
  lift: (press, event) => {
71
- if (!press.hasMoved && !this.isDisabled()) this.#pressBat(event);
72
71
  this.#isClickSwallowed = true;
72
+ if (press.hasMoved || this.isDisabled()) return;
73
+ if (!isUnder(this.group, event.clientX, event.clientY)) return;
74
+ this.#pressBat(event);
73
75
  },
74
76
  move: (press, event) => {
75
77
  if (this.isDisabled()) return;
@@ -19,6 +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
23
  var SonicToggle = class extends SonicPositionGroupElement {
23
24
  bare = renderBare();
24
25
  defaultOrientation = "horizontal";
@@ -60,17 +61,19 @@ var SonicToggle = class extends SonicPositionGroupElement {
60
61
  bindDrag(this.group, {
61
62
  grab: (event) => this.#grab(event),
62
63
  lift: (drag, event) => {
63
- if (drag.hasMoved || drag.isHolding || this.isDisabled()) return;
64
+ if (!drag.isPress || drag.isHolding || this.isDisabled()) return;
64
65
  if (!isUnder(this.group, event.clientX, event.clientY)) return;
65
66
  this.#press(drag.position);
66
67
  },
67
68
  move: (drag, event) => {
68
69
  if (drag.isHolding || this.isDisabled()) return;
69
70
  const pointer = this.along(event);
70
- if (!drag.hasMoved && Math.abs(pointer - drag.start) < drag.thresholdPx) return;
71
+ const moved = Math.abs(pointer - drag.start);
72
+ if (!drag.hasMoved && moved < drag.thresholdPx) return;
71
73
  drag.hasMoved = true;
72
74
  this.toggleState("dragging", true);
73
- this.#slideTo(positionAt(drag.travel, pointer), event.pointerId);
75
+ if (moved >= pressSlop * drag.thresholdPx) drag.isPress = false;
76
+ if (this.#slideTo(positionAt(drag.travel, pointer), event.pointerId)) drag.isPress = false;
74
77
  },
75
78
  release: (drag) => {
76
79
  delete drag.marked.dataset.sonicPressed;
@@ -95,6 +98,7 @@ var SonicToggle = class extends SonicPositionGroupElement {
95
98
  return {
96
99
  hasMoved: false,
97
100
  isHolding: this.#holdFrom(position, event.pointerId),
101
+ isPress: true,
98
102
  marked,
99
103
  position,
100
104
  start: this.along(event),
@@ -124,12 +128,13 @@ var SonicToggle = class extends SonicPositionGroupElement {
124
128
  const crossed = Math.round(at);
125
129
  this.#draggedTo = at;
126
130
  this.#placeCap(at);
127
- if (this.isChecked(crossed)) return;
131
+ if (this.isChecked(crossed)) return false;
128
132
  this.releaseHold();
129
- if (this.isOptionDisabled(crossed) || this.isChecked(crossed)) return;
133
+ if (this.isOptionDisabled(crossed) || this.isChecked(crossed)) return false;
130
134
  if (this.isMomentary(crossed)) this.hold(crossed, pointerId);
131
135
  else this.select(crossed);
132
136
  this.refocusAt(crossed);
137
+ return true;
133
138
  }
134
139
  #travel(event, isOnCap) {
135
140
  const [first, last] = enabledEnds(this.positions());
@@ -52,6 +52,9 @@ var SonicValueElement = class extends SonicFormElement {
52
52
  ];
53
53
  static {
54
54
  link = (element) => ({
55
+ change: () => {
56
+ element.dispatchEvent(new Event("change", { bubbles: true }));
57
+ },
55
58
  input: (next, isMover = false) => (isMover || !element.#isHeld()) && element.input(next),
56
59
  isDisabled: () => element.isDisabled(),
57
60
  isHeld: () => element.#isHeld(),
@@ -1,17 +1,13 @@
1
+ import { createDoublePress } from "../lib/double-press.js";
2
+
1
3
  //#region src/elements/value-entry.ts
2
- const doublePressMs = 500;
3
- const doublePressPx = 4;
4
- function isDoublePress(previous, next) {
5
- if (!previous) return false;
6
- return next.time - previous.time < doublePressMs && Math.hypot(next.x - previous.x, next.y - previous.y) < doublePressPx;
7
- }
8
4
  var ValueEntry = class {
9
5
  get isOpen() {
10
6
  return this.#isOpen;
11
7
  }
8
+ #doublePress = createDoublePress();
12
9
  #input;
13
10
  #isOpen = false;
14
- #lastPress;
15
11
  #openedText = "";
16
12
  #options;
17
13
  constructor(input, options) {
@@ -44,7 +40,7 @@ var ValueEntry = class {
44
40
  if (isCommitting && this.#input.value !== this.#openedText) this.#options.commit(this.#input.value);
45
41
  }
46
42
  forgetPress() {
47
- this.#lastPress = void 0;
43
+ this.#doublePress.forget();
48
44
  }
49
45
  open() {
50
46
  const input = this.#input;
@@ -57,16 +53,7 @@ var ValueEntry = class {
57
53
  input.select();
58
54
  }
59
55
  press(event) {
60
- const press = {
61
- time: event.timeStamp,
62
- x: event.clientX,
63
- y: event.clientY
64
- };
65
- const previous = this.#lastPress;
66
- this.#lastPress = press;
67
- if (!isDoublePress(previous, press)) return false;
68
- this.#lastPress = void 0;
69
- return true;
56
+ return this.#doublePress.press(event);
70
57
  }
71
58
  };
72
59
 
@@ -287,6 +287,14 @@ var SonicWaveform = class extends SonicWaveElement {
287
287
  }]));
288
288
  this.surface()?.rebuild();
289
289
  }
290
+ scrubChanged() {
291
+ const isScrubbing = this.scrubState().from !== void 0;
292
+ this.#grabbed = isScrubbing ? this.#grabbed ?? this.#lastDrawn() : void 0;
293
+ super.scrubChanged();
294
+ }
295
+ scrubsKeyRepeat() {
296
+ return true;
297
+ }
290
298
  #duePending() {
291
299
  const due = dueRegions(this.#pending, this.#pendingListedMs, [performance.now(), this.pendingDelay]);
292
300
  if (due.length < this.#pending.length) this.surface()?.requestFrame();
@@ -294,10 +302,7 @@ var SonicWaveform = class extends SonicWaveElement {
294
302
  }
295
303
  #grab() {
296
304
  if (this.#zoomGesture?.isPinching() === true) return void 0;
297
- const drawn = this.#drawn ?? {
298
- seconds: this.value,
299
- startSeconds: this.value
300
- };
305
+ const drawn = this.#lastDrawn();
301
306
  this.#grabbed = drawn;
302
307
  const mapping = this.mapping();
303
308
  const [low, high] = mapping.bounds;
@@ -317,6 +322,12 @@ var SonicWaveform = class extends SonicWaveElement {
317
322
  seconds: this.value === from ? grabbed.seconds : this.value
318
323
  };
319
324
  }
325
+ #lastDrawn() {
326
+ return this.#drawn ?? {
327
+ seconds: this.value,
328
+ startSeconds: this.value
329
+ };
330
+ }
320
331
  #readKindColours() {
321
332
  const kinds = [...new Set(this.markers.map(({ kind }) => kind).filter((kind) => isKind(kind)))];
322
333
  const probes = kinds.map((kind) => {
@@ -4,6 +4,7 @@ import { SonicFormElement } from "./form-element.js";
4
4
  import { requireChild, template } from "../lib/render.js";
5
5
  import { Readout } from "./readout.js";
6
6
  import { ReadoutClaim } from "./readout-claim.js";
7
+ import { createDoublePress } from "../lib/double-press.js";
7
8
  import { focusByPointer } from "../lib/focus-by-pointer.js";
8
9
  import { isMenuPress, isResetPress } from "../lib/modifier-press.js";
9
10
  import { resetKeys } from "../lib/value-mapping.js";
@@ -66,6 +67,15 @@ var SonicXy = class extends SonicFormElement {
66
67
  "y-step",
67
68
  "y-taper"
68
69
  ];
70
+ get doublePress() {
71
+ return this.getAttribute("double-press") === "reset" ? "reset" : "none";
72
+ }
73
+ set doublePress(gesture) {
74
+ this.reflect("double-press", gesture);
75
+ }
76
+ get dragging() {
77
+ return this.hasState("dragging");
78
+ }
69
79
  get formatValue() {
70
80
  return this.#formatValue;
71
81
  }
@@ -82,6 +92,9 @@ var SonicXy = class extends SonicFormElement {
82
92
  set readout(isEnabled) {
83
93
  this.reflect("readout", isEnabled);
84
94
  }
95
+ get revealed() {
96
+ return this.hasState("revealed");
97
+ }
85
98
  get x() {
86
99
  return this.#models.x.value;
87
100
  }
@@ -194,10 +207,12 @@ var SonicXy = class extends SonicFormElement {
194
207
  this.#renderReadout();
195
208
  });
196
209
  #currentAxis = "x";
210
+ #doublePress = createDoublePress();
197
211
  #xy = renderXy();
198
212
  #field = requireChild(this.#xy, ".sonic-xy-field", HTMLDivElement);
199
213
  #formatValue;
200
214
  #gesture;
215
+ #isRevealed = false;
201
216
  #models = {
202
217
  x: createValueModel(() => this.#spec("x")),
203
218
  y: createValueModel(() => this.#spec("y"))
@@ -214,10 +229,11 @@ var SonicXy = class extends SonicFormElement {
214
229
  this.#respec(name);
215
230
  }
216
231
  connectedCallback() {
217
- this.upgradeProperties("readout", "xDefault", "xLabel", "xMax", "xMidpoint", "xMin", "xOrigin", "xStep", "xTaper", "yDefault", "yLabel", "yMax", "yMidpoint", "yMin", "yOrigin", "yStep", "yTaper", "formatValue", "x", "y");
232
+ this.upgradeProperties("doublePress", "readout", "xDefault", "xLabel", "xMax", "xMidpoint", "xMin", "xOrigin", "xStep", "xTaper", "yDefault", "yLabel", "yMax", "yMidpoint", "yMin", "yOrigin", "yStep", "yTaper", "formatValue", "x", "y");
218
233
  super.connectedCallback();
219
234
  }
220
235
  formResetCallback() {
236
+ if (this.#gesture?.current()) return;
221
237
  this.#respec(...axes);
222
238
  }
223
239
  connect(signal) {
@@ -259,23 +275,26 @@ var SonicXy = class extends SonicFormElement {
259
275
  const move = keyMoves.get(event.key);
260
276
  if (!move && !(resetKeys.has(event.key) && this.#hasDefault())) return;
261
277
  event.preventDefault();
262
- if (move) this.#keyTo(...move);
263
- else this.#reset();
264
278
  this.#claim.reveal("keys");
265
279
  this.#renderReadout();
280
+ if (move) this.#keyTo(...move);
281
+ else this.#reset();
266
282
  }, { signal });
267
283
  xy.addEventListener("focusout", (event) => {
268
284
  if (event.relatedTarget instanceof Node && xy.contains(event.relatedTarget)) return;
269
285
  if (this.#claim.conceal("keys")) this.#renderReadout();
270
286
  }, { signal });
271
287
  signal.addEventListener("abort", () => {
272
- this.#claim.conceal("keys");
288
+ if (this.#claim.conceal("keys")) this.#renderReadout();
273
289
  }, { once: true });
274
290
  }
275
291
  #bindPointer(xy, signal) {
276
292
  xy.addEventListener("mousedown", (event) => {
277
293
  event.preventDefault();
278
294
  }, { signal });
295
+ xy.addEventListener("pointercancel", () => {
296
+ this.#doublePress.forget();
297
+ }, { signal });
279
298
  this.#gesture = bindFieldGesture(xy, {
280
299
  claim: this.#claim,
281
300
  grab: (event) => {
@@ -290,6 +309,10 @@ var SonicXy = class extends SonicFormElement {
290
309
  input: (_hold, next) => {
291
310
  if (!this.#input(next)) this.#renderReadout();
292
311
  },
312
+ lift: (_hold, event) => {
313
+ if (!this.#doublePress.press(event) || this.doublePress !== "reset") return;
314
+ this.#reset();
315
+ },
293
316
  release: (_hold, moved) => {
294
317
  if (moved.length > 0) this.dispatchEvent(new Event("change", { bubbles: true }));
295
318
  },
@@ -393,11 +416,16 @@ var SonicXy = class extends SonicFormElement {
393
416
  this.writeFormValue(fields, state);
394
417
  }
395
418
  #renderReadout() {
419
+ const { isRevealed } = this.#claim;
396
420
  this.#readout.show({
397
421
  anchor: this.#puck,
398
- isOpen: this.#claim.isRevealed && this.readout,
422
+ isOpen: isRevealed && this.readout,
399
423
  text: this.#valueText("x")
400
424
  });
425
+ if (isRevealed === this.#isRevealed) return;
426
+ this.#isRevealed = isRevealed;
427
+ this.toggleState("revealed", isRevealed);
428
+ this.dispatchEvent(new Event("sonic-reveal", { bubbles: true }));
401
429
  }
402
430
  #reset() {
403
431
  this.#commit({