@xsynaptic/sonic-ui 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +122 -19
  3. package/dist/default/elements/button.js +2 -5
  4. package/dist/default/elements/dial.js +2 -7
  5. package/dist/default/elements/display-element.js +9 -12
  6. package/dist/default/elements/envelope.js +31 -78
  7. package/dist/default/elements/field-gesture.js +81 -0
  8. package/dist/default/elements/meter.js +4 -1
  9. package/dist/default/elements/number.js +0 -7
  10. package/dist/default/elements/position-group.js +42 -26
  11. package/dist/default/elements/radio-group.js +59 -56
  12. package/dist/default/elements/scale.js +3 -3
  13. package/dist/default/elements/segmented.js +0 -5
  14. package/dist/default/elements/slider.js +11 -20
  15. package/dist/default/elements/sonic-element.js +3 -1
  16. package/dist/default/elements/split.js +14 -13
  17. package/dist/default/elements/switch.js +31 -47
  18. package/dist/default/elements/toggle.js +20 -49
  19. package/dist/default/elements/value-element.js +72 -94
  20. package/dist/default/elements/value-gestures.js +22 -22
  21. package/dist/default/elements/wave-element.js +16 -20
  22. package/dist/default/elements/waveform.js +42 -241
  23. package/dist/default/elements/wavestrip.js +10 -5
  24. package/dist/default/elements/xy.js +67 -119
  25. package/dist/default/index.js +2 -1
  26. package/dist/default/lib/canvas-surface.js +25 -0
  27. package/dist/default/lib/clock.js +23 -0
  28. package/dist/default/lib/crossfade.js +1 -0
  29. package/dist/default/lib/envelope-curve.js +1 -0
  30. package/dist/default/lib/peaks-request.js +44 -0
  31. package/dist/default/lib/positions.js +58 -0
  32. package/dist/default/lib/scrub.js +12 -0
  33. package/dist/default/lib/spoken-duration.js +30 -0
  34. package/dist/default/lib/value-model.js +62 -0
  35. package/dist/default/lib/waveform-buckets.js +7 -1
  36. package/dist/default/lib/waveform-scene.js +108 -0
  37. package/dist/default/lib/zoom-gesture.js +97 -1
  38. package/dist/dev/elements/button.d.ts +2 -0
  39. package/dist/dev/elements/button.js +14 -5
  40. package/dist/dev/elements/button.js.map +1 -1
  41. package/dist/dev/elements/dial.d.ts +2 -0
  42. package/dist/dev/elements/dial.js +16 -7
  43. package/dist/dev/elements/dial.js.map +1 -1
  44. package/dist/dev/elements/display-element.d.ts +5 -3
  45. package/dist/dev/elements/display-element.js +11 -9
  46. package/dist/dev/elements/display-element.js.map +1 -1
  47. package/dist/dev/elements/envelope.js +48 -78
  48. package/dist/dev/elements/envelope.js.map +1 -1
  49. package/dist/dev/elements/field-gesture.js +82 -0
  50. package/dist/dev/elements/field-gesture.js.map +1 -0
  51. package/dist/dev/elements/meter.d.ts +10 -5
  52. package/dist/dev/elements/meter.js +5 -1
  53. package/dist/dev/elements/meter.js.map +1 -1
  54. package/dist/dev/elements/number.js +13 -7
  55. package/dist/dev/elements/number.js.map +1 -1
  56. package/dist/dev/elements/position-group.d.ts +12 -6
  57. package/dist/dev/elements/position-group.js +42 -26
  58. package/dist/dev/elements/position-group.js.map +1 -1
  59. package/dist/dev/elements/radio-group.d.ts +11 -7
  60. package/dist/dev/elements/radio-group.js +59 -56
  61. package/dist/dev/elements/radio-group.js.map +1 -1
  62. package/dist/dev/elements/scale.js +3 -3
  63. package/dist/dev/elements/scale.js.map +1 -1
  64. package/dist/dev/elements/segmented.js +12 -5
  65. package/dist/dev/elements/segmented.js.map +1 -1
  66. package/dist/dev/elements/slider.d.ts +4 -2
  67. package/dist/dev/elements/slider.js +25 -20
  68. package/dist/dev/elements/slider.js.map +1 -1
  69. package/dist/dev/elements/sonic-element.d.ts +13 -2
  70. package/dist/dev/elements/sonic-element.js +63 -11
  71. package/dist/dev/elements/sonic-element.js.map +1 -1
  72. package/dist/dev/elements/split.d.ts +1 -0
  73. package/dist/dev/elements/split.js +14 -13
  74. package/dist/dev/elements/split.js.map +1 -1
  75. package/dist/dev/elements/switch.d.ts +2 -2
  76. package/dist/dev/elements/switch.js +38 -47
  77. package/dist/dev/elements/switch.js.map +1 -1
  78. package/dist/dev/elements/toggle.js +32 -49
  79. package/dist/dev/elements/toggle.js.map +1 -1
  80. package/dist/dev/elements/value-element.d.ts +11 -5
  81. package/dist/dev/elements/value-element.js +72 -94
  82. package/dist/dev/elements/value-element.js.map +1 -1
  83. package/dist/dev/elements/value-gestures.js +22 -22
  84. package/dist/dev/elements/value-gestures.js.map +1 -1
  85. package/dist/dev/elements/wave-element.d.ts +8 -5
  86. package/dist/dev/elements/wave-element.js +30 -20
  87. package/dist/dev/elements/wave-element.js.map +1 -1
  88. package/dist/dev/elements/waveform.d.ts +20 -22
  89. package/dist/dev/elements/waveform.js +42 -241
  90. package/dist/dev/elements/waveform.js.map +1 -1
  91. package/dist/dev/elements/wavestrip.d.ts +5 -1
  92. package/dist/dev/elements/wavestrip.js +10 -5
  93. package/dist/dev/elements/wavestrip.js.map +1 -1
  94. package/dist/dev/elements/xy.js +81 -119
  95. package/dist/dev/elements/xy.js.map +1 -1
  96. package/dist/dev/index.d.ts +5 -3
  97. package/dist/dev/index.js +2 -1
  98. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  99. package/dist/dev/lib/canvas-surface.js +25 -0
  100. package/dist/dev/lib/canvas-surface.js.map +1 -1
  101. package/dist/dev/lib/clock.d.ts +5 -0
  102. package/dist/dev/lib/clock.js +24 -0
  103. package/dist/dev/lib/clock.js.map +1 -0
  104. package/dist/dev/lib/crossfade.d.ts +1 -0
  105. package/dist/dev/lib/crossfade.js +1 -0
  106. package/dist/dev/lib/crossfade.js.map +1 -1
  107. package/dist/dev/lib/envelope-curve.d.ts +1 -0
  108. package/dist/dev/lib/envelope-curve.js +1 -0
  109. package/dist/dev/lib/envelope-curve.js.map +1 -1
  110. package/dist/dev/lib/mirror-children.js.map +1 -1
  111. package/dist/dev/lib/owned-control.js.map +1 -1
  112. package/dist/dev/lib/peaks-request.d.ts +5 -0
  113. package/dist/dev/lib/peaks-request.js +45 -0
  114. package/dist/dev/lib/peaks-request.js.map +1 -0
  115. package/dist/dev/lib/positions.d.ts +7 -0
  116. package/dist/dev/lib/positions.js +59 -0
  117. package/dist/dev/lib/positions.js.map +1 -0
  118. package/dist/dev/lib/scrub.d.ts +8 -0
  119. package/dist/dev/lib/scrub.js +13 -0
  120. package/dist/dev/lib/scrub.js.map +1 -0
  121. package/dist/dev/lib/spoken-duration.js +31 -0
  122. package/dist/dev/lib/spoken-duration.js.map +1 -0
  123. package/dist/dev/lib/time-regions.d.ts +1 -0
  124. package/dist/dev/lib/time-regions.js.map +1 -1
  125. package/dist/dev/lib/units.d.ts +8 -0
  126. package/dist/dev/lib/value-model.js +63 -0
  127. package/dist/dev/lib/value-model.js.map +1 -0
  128. package/dist/dev/lib/waveform-buckets.d.ts +2 -1
  129. package/dist/dev/lib/waveform-buckets.js +7 -1
  130. package/dist/dev/lib/waveform-buckets.js.map +1 -1
  131. package/dist/dev/lib/waveform-scene.d.ts +11 -0
  132. package/dist/dev/lib/waveform-scene.js +109 -0
  133. package/dist/dev/lib/waveform-scene.js.map +1 -0
  134. package/dist/dev/lib/zoom-gesture.js +97 -1
  135. package/dist/dev/lib/zoom-gesture.js.map +1 -1
  136. package/dist/skins/amber.css +1 -0
  137. package/dist/skins/ivory.css +1 -0
  138. package/dist/skins/lime.css +1 -0
  139. package/dist/skins/slate.css +1 -0
  140. package/dist/styles/button.css +45 -11
  141. package/dist/styles/dial.css +10 -9
  142. package/dist/styles/envelope.css +10 -4
  143. package/dist/styles/led.css +5 -1
  144. package/dist/styles/material/arc.css +3 -2
  145. package/dist/styles/material/bracket.css +2 -2
  146. package/dist/styles/material/cap.css +33 -12
  147. package/dist/styles/material/core.css +4 -0
  148. package/dist/styles/material/glass.css +1 -1
  149. package/dist/styles/material/groove.css +4 -3
  150. package/dist/styles/material/keycap.css +3 -2
  151. package/dist/styles/material/lens.css +1 -1
  152. package/dist/styles/material/pane.css +5 -4
  153. package/dist/styles/material/readout.css +6 -6
  154. package/dist/styles/material/well.css +2 -2
  155. package/dist/styles/meter.css +2 -7
  156. package/dist/styles/number.css +5 -1
  157. package/dist/styles/panel.css +4 -3
  158. package/dist/styles/ring.css +1 -1
  159. package/dist/styles/screen.css +5 -1
  160. package/dist/styles/segmented.css +17 -4
  161. package/dist/styles/slider.css +9 -3
  162. package/dist/styles/spectrum.css +16 -3
  163. package/dist/styles/switch.css +35 -10
  164. package/dist/styles/toggle.css +14 -7
  165. package/dist/styles/waveform.css +17 -5
  166. package/dist/styles/wavestrip.css +11 -3
  167. package/dist/styles/xy.css +9 -5
  168. package/package.json +1 -6
  169. package/dist/default/dat.js +0 -51
  170. package/dist/default/lib/asked-value.js +0 -21
  171. package/dist/default/lib/fill-watch.js +0 -29
  172. package/dist/dev/dat.d.ts +0 -19
  173. package/dist/dev/dat.js +0 -52
  174. package/dist/dev/dat.js.map +0 -1
  175. package/dist/dev/lib/asked-value.js +0 -22
  176. package/dist/dev/lib/asked-value.js.map +0 -1
  177. package/dist/dev/lib/fill-watch.js +0 -30
  178. package/dist/dev/lib/fill-watch.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @xsynaptic/sonic-ui
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - - Shared tokens, each unset or at today's value: `--sonic-press-duration` (60ms) and `--sonic-fade-duration` (120ms) time a cap's press and every fade; `--sonic-shade` (black) and `--sonic-shine` (white) are the poles the light mixes a surface towards.
8
+ - `--sonic-legend-font-size` sets the text of a button, a segmented option, a switch and a toggle. `--sonic-radius` sets the outer corner of a button, a segmented control, a number box, a screen, a slider's cap, a wave strip, a waveform, a spectrum, an envelope and an XY pad. A control's own `-font-ratio` or `-radius-ratio` still wins, and under the shared radius a button's face follows its corner.
9
+ - Waveform and wave strip: the inset ratio now defaults to 0, so the wave fills its glass; set `--sonic-waveform-inset-ratio: 0.06` or `--sonic-wavestrip-inset-ratio: 0.1` for the inset each had. The slate, amber, ivory and lime skins keep the wave strip's 0.1, which holds its markers clear of the well's shadow.
10
+ - Spectrum and wave strip: the canvas is rounded to the glass, so a large radius no longer leaves corners poking out.
11
+ - A cast shadow or highlight now computes to `color(srgb …)` where it was `rgba()`; nothing drawn changes.
12
+ - In development a control warns about a box property on its host, a padding zeroed by an unlayered rule, and a missing `material/scale.css` or `material/bracket.css`. The README has the recipe for layering a reset.
13
+ - - A waveform or a wave strip now shows, reads and speaks a clock when no formatter is set: `1:23` in the readout and the typed entry, and a duration in the nearest `lang` for assistive technology. Setting `formatValue`, `parseValue` or `formatSpokenValue` replaces that part.
14
+ - `formatClock` and `parseClock` are exported, so your own time text can match. A negative time is written with `−` (U+2212), not the ASCII hyphen; `parseClock` reads either.
15
+ - `fullScale` is optional on `peaks` and follows the array when unset.
16
+ - `./dat` is removed. Pass `peaks` as `{ pairsPerSecond, samples }`, where the first is a header's `sampleRate / samplesPerPixel`.
17
+
18
+ ## 0.18.0
19
+
20
+ ### Minor Changes
21
+
22
+ - - Button: an icon keeps its `viewBox` proportions at `--sonic-button-icon-ratio` of the button's height, and the button widens to fit it, never under `--sonic-button-aspect-ratio`; at 0 the button is as wide as its icon. Breaking for an icon wider than it is tall, which was shrunk into a square.
23
+ - Waveform: a dimmed marker's label dims by opacity, so `renderLabel` children with their own colour dim too, as does the label's scrim. Drop any rule that dimmed them by hand.
24
+
25
+ ### Patch Changes
26
+
27
+ - Internals restructured into smaller modules; no change to the public surface.
28
+ - Switch and toggle internals restructured around position indices; no change to the public surface.
29
+ - The types say the unit or the contract on the properties whose type alone does not, such as a meter's `level` and a waveform's `zoom`.
30
+
3
31
  ## 0.17.0
4
32
 
5
33
  ### Minor Changes
package/README.md CHANGED
@@ -4,6 +4,14 @@ Native custom elements for audio interfaces. Each control renders in the light D
4
4
 
5
5
  Early and unstable: any 0.x release may break the API.
6
6
 
7
+ This README holds what the package's own files do not say. For the rest, read the package:
8
+
9
+ - Properties and methods: `dist/dev/elements/<control>.d.ts`. Every attribute has a property of the same name in camel case.
10
+ - Tokens and their defaults: `dist/styles/<control>.css` and `dist/styles/material/`, where each token is read as `var(--sonic-…, default)`.
11
+ - Working markup for every control: `playground/src/components/specimens/` in the repository.
12
+
13
+ ## Setup
14
+
7
15
  ```sh
8
16
  pnpm add @xsynaptic/sonic-ui
9
17
  ```
@@ -20,12 +28,121 @@ import '@xsynaptic/sonic-ui/define';
20
28
  ```
21
29
 
22
30
  ```html
23
- <sonic-dial aria-label="Cutoff" value="40"></sonic-dial>
31
+ <body class="sonic-skin-slate">
32
+ <sonic-dial aria-label="Cutoff" value="40"></sonic-dial>
33
+ </body>
24
34
  ```
25
35
 
26
- A display takes its signal as data: `spectrum.analyser = node`, or `spectrum.push(frame)`.
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
+ ```
44
+
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 missing sheet, 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.
48
+
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.
53
+ - 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 pane of glass for your own text or canvas, and `class="sonic-panel"` groups controls under a `sonic-panel-title`.
104
+
105
+ ## Displays
106
+
107
+ A display takes its signal as data and never opens an `AudioContext`.
108
+
109
+ - `<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.
110
+ - `<sonic-spectrum>`: set `analyser` to an `AnalyserNode`, or call `push(decibels)` with a `Float32Array` holding one level per bin and set `sample-rate`.
111
+
112
+ ## Controls that drive other controls
113
+
114
+ - `<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.
115
+ - `<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.
116
+ - `<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`.
117
+ - `envelopeCurve` and `crossfadeGains` are exported so your DSP computes what the control draws.
118
+
119
+ ## Wave controls
120
+
121
+ `<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.
122
+
123
+ 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.
124
+
125
+ 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.
126
+
127
+ 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.
128
+
129
+ 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.
130
+
131
+ 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.
132
+
133
+ 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.
134
+
135
+ 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.
136
+
137
+ ## Skinning
27
138
 
28
- Under a hydrating framework, register the definitions once hydration is done (in React, from an effect); a control owns one child of its host and uses only the children it documents.
139
+ 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.
140
+
141
+ - Set tokens, not rules on hook classes. If a look needs a hook-class rule, a token is missing.
142
+ - Most geometry tokens end in `-ratio` and are a unitless share of the control's size, so one size token scales the whole control.
143
+ - A length token takes any CSS length except a percentage. For a fluid size use container units.
144
+ - `--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.
145
+ - `--sonic-target-size` widens the region that takes a press past the control's box. It is unset by default.
29
146
 
30
147
  ## Loading less
31
148
 
@@ -58,20 +175,6 @@ Under a hydrating framework, register the definitions once hydration is done (in
58
175
  | `wavestrip.css` | core, cap, glass, pane, well, readout, value |
59
176
  | `xy.css` | core, cap, glass, pane, well, readout, bracket |
60
177
 
61
- An LED inside a control also needs `led.css`, and a button inside a ring needs `button.css`. The development build warns about a missing sheet.
62
-
63
- ## Wave controls
64
-
65
- `<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.
66
-
67
- 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.
68
-
69
- 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.
70
-
71
- 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.
72
-
73
- 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.
74
-
75
- 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.
178
+ An LED inside a control also needs `led.css`, and a button inside a ring needs `button.css`.
76
179
 
77
- Targets current Chrome and Edge, Safari 26 and Firefox 147. MIT licensed.
180
+ Targets current Chrome and Edge, Safari 26 and Firefox 147. No control mirrors for right-to-left. MIT licensed.
@@ -54,6 +54,7 @@ var SonicButton = class extends SonicFormElement {
54
54
  set latching(isLatching) {
55
55
  this.reflect("latching", isLatching);
56
56
  }
57
+ /** Shows the legend child whose `data-sonic-when` has this name */
57
58
  get legend() {
58
59
  return this.getAttribute("legend") ?? void 0;
59
60
  }
@@ -80,6 +81,7 @@ var SonicButton = class extends SonicFormElement {
80
81
  set softDisabled(isSoftDisabled) {
81
82
  this.reflect("soft-disabled", isSoftDisabled);
82
83
  }
84
+ /** Submitted with the form while a latching button is pressed */
83
85
  get value() {
84
86
  return this.getAttribute("value") ?? "on";
85
87
  }
@@ -114,11 +116,6 @@ var SonicButton = class extends SonicFormElement {
114
116
  }
115
117
  }, signal);
116
118
  this.render();
117
- this.checkStyles(button, "button.css", [
118
- "cap",
119
- "well",
120
- "keycap"
121
- ]);
122
119
  button.addEventListener("click", (event) => {
123
120
  if (this.#isSoftDisabled()) {
124
121
  event.preventDefault();
@@ -37,12 +37,14 @@ var SonicDial = class extends SonicValueElement {
37
37
  set endless(isEndless) {
38
38
  this.reflect("endless", isEndless);
39
39
  }
40
+ /** How far a modulation source can push the value, in value units; signed */
40
41
  get modulation() {
41
42
  return this.numberAttribute("modulation", 0);
42
43
  }
43
44
  set modulation(amount) {
44
45
  this.reflect("modulation", amount);
45
46
  }
47
+ /** Where the value is right now with modulation applied; a property only, cheap to write every frame, fires nothing */
46
48
  get modulationValue() {
47
49
  return this.#modulation.value();
48
50
  }
@@ -64,13 +66,6 @@ var SonicDial = class extends SonicValueElement {
64
66
  marks: requireChild(dial, ".sonic-dial-scale", HTMLDivElement)
65
67
  }, signal);
66
68
  this.render();
67
- this.checkStyles(dial, "dial.css", [
68
- "cap",
69
- "readout",
70
- "value",
71
- ["glass", ".sonic-dial-readout"],
72
- ["arc", ".sonic-dial-ring"]
73
- ]);
74
69
  this.bindGestures(dial, signal, () => ({
75
70
  position: (event) => -event.clientY,
76
71
  travelPx: this.travelPx(dial, "--_sonic-dial-travel")
@@ -1,6 +1,5 @@
1
1
  import { SonicElement } from "./sonic-element.js";
2
2
  import { bindSurface } from "../lib/canvas-surface.js";
3
- import { bindFill } from "../lib/fill-watch.js";
4
3
 
5
4
  //#region src/elements/display-element.ts
6
5
  var SonicDisplayElement = class extends SonicElement {
@@ -38,17 +37,17 @@ var SonicDisplayElement = class extends SonicElement {
38
37
  #analyser;
39
38
  #buffer = /* @__PURE__ */ new Float32Array(0);
40
39
  #surface;
41
- #syncFill;
42
40
  attributeChangedCallback(name) {
43
- if (name === "fill") this.#syncFill?.(this.fill);
41
+ if (name === "fill") this.#surface?.setFill(this.fill);
44
42
  this.#surface?.invalidate();
45
43
  }
46
44
  connectedCallback() {
47
45
  this.upgradeProperties("analyser", "fill", "max", "min");
48
46
  super.connectedCallback();
49
47
  }
50
- push(frame) {
51
- this.receive(frame);
48
+ /** One level per bin, as `AnalyserNode.getFloatFrequencyData` fills it */
49
+ push(decibels) {
50
+ this.receive(decibels);
52
51
  this.#surface?.requestFrame();
53
52
  }
54
53
  repaint() {
@@ -60,19 +59,17 @@ var SonicDisplayElement = class extends SonicElement {
60
59
  this.#surface = bindSurface({
61
60
  canvas: this.canvas,
62
61
  colours: this.colours,
62
+ fill: {
63
+ control,
64
+ sizeProperty: this.sizeProperty
65
+ },
63
66
  numbers: this.numbers,
64
67
  paint: (context, frame) => {
65
68
  this.#paint(context, frame);
66
69
  },
67
70
  signal
68
71
  });
69
- this.checkStyles(control, this.sheet, [
70
- "glass",
71
- "pane",
72
- "well"
73
- ]);
74
- this.#syncFill = bindFill(control, this.sizeProperty, signal);
75
- this.#syncFill(this.fill);
72
+ this.#surface.setFill(this.fill);
76
73
  }
77
74
  surface() {
78
75
  return this.#surface;
@@ -1,13 +1,11 @@
1
1
  import { writeAttribute } from "../lib/write-attribute.js";
2
2
  import { SonicElement } from "./sonic-element.js";
3
3
  import { requireChild, template } from "../lib/render.js";
4
- import { bindDrag } from "../lib/pointer-drag.js";
5
4
  import { Readout } from "./readout.js";
6
- import { ReadoutClaim, revealDelay } from "./readout-claim.js";
7
- import { dragThresholdPx } from "../lib/drag-step.js";
5
+ import { ReadoutClaim } from "./readout-claim.js";
8
6
  import { SonicValueElement, linkValue } from "./value-element.js";
7
+ import { bindFieldGesture } from "./field-gesture.js";
9
8
  import { adsrPath, adsrShape, adsrStages, timeStages } from "../lib/adsr-shape.js";
10
- import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
11
9
 
12
10
  //#region src/elements/envelope.ts
13
11
  const curveStages = [
@@ -22,12 +20,6 @@ const bindable = [
22
20
  "release-curve"
23
21
  ];
24
22
  const bindableNames = new Set(bindable);
25
- function flip(point, isFlipped) {
26
- return isFlipped ? {
27
- ...point,
28
- y: -point.y
29
- } : point;
30
- }
31
23
  function isParent(node) {
32
24
  return "querySelector" in node;
33
25
  }
@@ -104,7 +96,7 @@ var SonicEnvelope = class extends SonicElement {
104
96
  this.reflect("hold", id);
105
97
  }
106
98
  get pointerType() {
107
- return this.#pointerType;
99
+ return this.#gesture?.pointerType();
108
100
  }
109
101
  get readout() {
110
102
  return this.hasAttribute("readout");
@@ -139,13 +131,12 @@ var SonicEnvelope = class extends SonicElement {
139
131
  #curveHandles = stageParts(this.#envelope, "sonic-envelope-curve", curveStages);
140
132
  #fill = requireChild(this.#envelope, ".sonic-envelope-fill", SVGElement);
141
133
  #floor = requireChild(this.#envelope, ".sonic-envelope-floor", SVGElement);
134
+ #gesture;
142
135
  #graph = requireChild(this.#envelope, ".sonic-envelope-graph", SVGElement);
143
136
  #handles = stageParts(this.#envelope, "sonic-envelope-handle", timeStages);
144
137
  #held;
145
138
  #isDriving = false;
146
139
  #line = requireChild(this.#envelope, ".sonic-envelope-line", SVGElement);
147
- #pointerDrag;
148
- #pointerType;
149
140
  #readout = new Readout(requireChild(this.#envelope, ".sonic-envelope-readout", HTMLDivElement));
150
141
  #shape = adsrShape({});
151
142
  #signal;
@@ -154,7 +145,7 @@ var SonicEnvelope = class extends SonicElement {
154
145
  attributeChangedCallback(name) {
155
146
  if (!this.#signal) return;
156
147
  if (name === "disabled") {
157
- this.#pointerDrag?.end();
148
+ this.#gesture?.end();
158
149
  this.#draw();
159
150
  return;
160
151
  }
@@ -170,12 +161,6 @@ var SonicEnvelope = class extends SonicElement {
170
161
  this.#signal = signal;
171
162
  this.keepControl(envelope, signal);
172
163
  this.#bind();
173
- this.checkStyles(envelope, "envelope.css", [
174
- "glass",
175
- "pane",
176
- "well",
177
- "readout"
178
- ]);
179
164
  this.#bindPointer(envelope, signal);
180
165
  signal.addEventListener("abort", () => {
181
166
  this.#signal = void 0;
@@ -183,13 +168,13 @@ var SonicEnvelope = class extends SonicElement {
183
168
  }, { once: true });
184
169
  }
185
170
  #bind() {
186
- this.#pointerDrag?.end();
171
+ this.#gesture?.end();
187
172
  this.#unbind();
188
173
  for (const name of bindable) {
189
174
  const element = this.#resolve(name);
190
175
  if (!element) continue;
191
176
  const link = linkValue(element);
192
- const unwatch = link.watch(() => {
177
+ const unwatch = link.model.watch(() => {
193
178
  if (!this.#isDriving) this.#draw();
194
179
  });
195
180
  this.#bindings.set(name, {
@@ -201,21 +186,23 @@ var SonicEnvelope = class extends SonicElement {
201
186
  this.#draw();
202
187
  }
203
188
  #bindPointer(envelope, signal) {
204
- this.#pointerDrag = bindDrag(envelope, {
205
- cancel: (drag) => {
206
- for (const { binding, from } of drag.drives) binding.link.input(from);
207
- },
189
+ this.#gesture = bindFieldGesture(envelope, {
190
+ claim: this.#claim,
208
191
  grab: (event) => this.#grab(event),
209
- move: (drag, event) => {
210
- this.#moveDrag(drag, event);
192
+ input: ({ drives }, next) => {
193
+ this.#isDriving = true;
194
+ for (const { axis, binding } of drives) {
195
+ const value = next[axis];
196
+ if (value !== void 0) binding.link.input(value);
197
+ }
198
+ this.#isDriving = false;
199
+ this.#draw();
211
200
  },
212
- release: (drag) => {
213
- this.#claim.press(false);
214
- for (const { binding, from } of drag.drives) {
215
- if (binding.link.value() === from) continue;
201
+ release: ({ drives }, moved) => {
202
+ for (const { axis, binding } of drives) {
203
+ if (!moved.includes(axis)) continue;
216
204
  binding.element.dispatchEvent(new Event("change", { bubbles: true }));
217
205
  }
218
- this.#pointerType = void 0;
219
206
  },
220
207
  toggle: (isDragging) => {
221
208
  this.toggleState("dragging", isDragging);
@@ -278,57 +265,23 @@ var SonicEnvelope = class extends SonicElement {
278
265
  #grab(event) {
279
266
  const pressed = this.#pressed(event);
280
267
  if (!pressed || pressed.drives.length === 0) return void 0;
281
- const { drives, isFlipped, part } = pressed;
282
- this.#pointerType = event.pointerType;
283
- this.#claim.press(true, revealDelay(this.#envelope));
284
- this.#held = part;
268
+ this.#held = pressed.part;
285
269
  this.#drawBracket();
286
270
  const box = this.#graph.getBoundingClientRect();
287
271
  const travel = {
288
272
  x: Math.max(1, box.width * this.#shape.share),
289
273
  y: Math.max(1, box.height)
290
274
  };
291
- const start = (axis) => {
292
- const link = drives.find((drive) => drive.axis === axis)?.binding.link;
293
- if (!link) return;
294
- const mapping = link.mapping();
295
- const from = link.value();
296
- return {
297
- from,
298
- mapping,
299
- proportion: mapping.proportionOf(from),
300
- travelPx: travel[axis]
301
- };
275
+ const axes = {};
276
+ for (const { axis, binding } of pressed.drives) axes[axis] = {
277
+ model: binding.link.model,
278
+ travelPx: travel[axis]
302
279
  };
303
280
  return {
304
- drives: drives.map((drive) => ({
305
- ...drive,
306
- from: drive.binding.link.value()
307
- })),
308
- isFlipped,
309
- part,
310
- state: startFieldDrag({
311
- position: flip(pointerPosition(event), isFlipped),
312
- thresholdPx: dragThresholdPx(event.pointerType),
313
- x: start("x"),
314
- y: start("y")
315
- })
281
+ ...pressed,
282
+ axes
316
283
  };
317
284
  }
318
- #moveDrag(drag, event) {
319
- const mappings = {};
320
- for (const { axis, binding } of drag.drives) mappings[axis] = binding.link.mapping();
321
- const step = stepFieldDrag(mappings, drag.state, flip(pointerMove(event), drag.isFlipped));
322
- drag.state = step.state;
323
- if (step.state.isEngaged) this.#claim.reveal("drag");
324
- this.#isDriving = true;
325
- for (const { axis, binding } of drag.drives) {
326
- const next = step[axis];
327
- if (next !== void 0) binding.link.input(next);
328
- }
329
- this.#isDriving = false;
330
- this.#draw();
331
- }
332
285
  #pressed(event) {
333
286
  const part = event.target instanceof Element ? event.target.closest(".sonic-envelope-handle, .sonic-envelope-curve") : void 0;
334
287
  const stage = part?.dataset.sonicStage;
@@ -344,10 +297,10 @@ var SonicEnvelope = class extends SonicElement {
344
297
  const proportions = {};
345
298
  const curves = {};
346
299
  for (const stage of adsrStages) {
347
- const link = this.#bindings.get(stage)?.link;
348
- proportions[stage] = link?.mapping().proportionOf(link.value());
300
+ const model = this.#bindings.get(stage)?.link.model;
301
+ proportions[stage] = model?.mapping().proportionOf(model.value);
349
302
  }
350
- for (const stage of curveStages) curves[stage] = this.#bindings.get(`${stage}-curve`)?.link.value();
303
+ for (const stage of curveStages) curves[stage] = this.#bindings.get(`${stage}-curve`)?.link.model.value;
351
304
  return adsrShape(proportions, curves);
352
305
  }
353
306
  #rebindOnce(key, arrives) {
@@ -359,7 +312,7 @@ var SonicEnvelope = class extends SonicElement {
359
312
  });
360
313
  }
361
314
  #renderReadout() {
362
- const drag = this.#pointerDrag?.current();
315
+ const drag = this.#gesture?.current();
363
316
  if (!drag) {
364
317
  this.#readout.close();
365
318
  return;
@@ -0,0 +1,81 @@
1
+ import { bindDrag } from "../lib/pointer-drag.js";
2
+ import { revealDelay } from "./readout-claim.js";
3
+ import { dragThresholdPx } from "../lib/drag-step.js";
4
+ import { pointerMove, pointerPosition, startFieldDrag, stepFieldDrag } from "../lib/field.js";
5
+
6
+ //#region src/elements/field-gesture.ts
7
+ const fieldAxes = ["x", "y"];
8
+ function flip(point, hold) {
9
+ return hold.isFlipped === true ? {
10
+ ...point,
11
+ y: -point.y
12
+ } : point;
13
+ }
14
+ function startAxis(hold) {
15
+ if (!hold) return;
16
+ const { model, travelPx } = hold;
17
+ const mapping = model.mapping();
18
+ const from = model.value;
19
+ return {
20
+ from,
21
+ mapping,
22
+ proportion: hold.proportion ?? mapping.proportionOf(from),
23
+ travelPx
24
+ };
25
+ }
26
+ function bindFieldGesture(target, options, signal) {
27
+ const { claim } = options;
28
+ let pointerType;
29
+ const drag = bindDrag(target, {
30
+ cancel: ({ from, hold }) => {
31
+ options.input(hold, from);
32
+ },
33
+ grab: (event) => {
34
+ pointerType = event.pointerType;
35
+ const hold = options.grab(event);
36
+ if (!hold) {
37
+ pointerType = void 0;
38
+ return;
39
+ }
40
+ const { x, y } = hold.axes;
41
+ claim.press(true, revealDelay(target));
42
+ return {
43
+ from: {
44
+ x: x?.from ?? x?.model.value,
45
+ y: y?.from ?? y?.model.value
46
+ },
47
+ hold,
48
+ state: startFieldDrag({
49
+ position: flip(pointerPosition(event), hold),
50
+ thresholdPx: dragThresholdPx(event.pointerType),
51
+ x: startAxis(x),
52
+ y: startAxis(y)
53
+ })
54
+ };
55
+ },
56
+ move: (held, event) => {
57
+ const { axes } = held.hold;
58
+ const step = stepFieldDrag({
59
+ x: axes.x?.model.mapping(),
60
+ y: axes.y?.model.mapping()
61
+ }, held.state, flip(pointerMove(event), held.hold));
62
+ held.state = step.state;
63
+ if (step.state.isEngaged) claim.reveal("drag");
64
+ options.input(held.hold, step);
65
+ },
66
+ release: ({ from, hold }) => {
67
+ claim.press(false);
68
+ options.release(hold, fieldAxes.filter((axis) => hold.axes[axis]?.model.value !== from[axis]));
69
+ pointerType = void 0;
70
+ },
71
+ toggle: options.toggle
72
+ }, signal);
73
+ return {
74
+ current: () => drag.current()?.hold,
75
+ end: drag.end,
76
+ pointerType: () => pointerType
77
+ };
78
+ }
79
+
80
+ //#endregion
81
+ export { bindFieldGesture };
@@ -49,6 +49,7 @@ var SonicMeter = class extends SonicElement {
49
49
  set ballistics(mode) {
50
50
  this.reflect("ballistics", mode);
51
51
  }
52
+ /** Linear peak amplitude, 1 at full scale; the meter converts to decibels and applies its ballistics */
52
53
  get level() {
53
54
  return this.#level;
54
55
  }
@@ -90,15 +91,18 @@ var SonicMeter = class extends SonicElement {
90
91
  set origin(value) {
91
92
  this.reflect("origin", value);
92
93
  }
94
+ /** Highest level since `resetPeak()`; `-Infinity` before any signal */
93
95
  get peak() {
94
96
  return this.#peak;
95
97
  }
98
+ /** Ladder thresholds, one per segment; unset draws a bar */
96
99
  get segments() {
97
100
  return parseNumberList(this.getAttribute("segments"));
98
101
  }
99
102
  set segments(thresholds) {
100
103
  this.reflect("segments", thresholds?.join(" "));
101
104
  }
105
+ /** A reading in the meter's own units, drawn as given with no ballistics; takes over from `level` while set */
102
106
  get value() {
103
107
  return this.optionalNumberAttribute("value");
104
108
  }
@@ -138,7 +142,6 @@ var SonicMeter = class extends SonicElement {
138
142
  this.#renderSegments();
139
143
  this.#render(now);
140
144
  this.#schedule(now);
141
- this.checkStyles(this.#meter, "meter.css", ["lens", "groove"]);
142
145
  signal.addEventListener("abort", () => {
143
146
  if (this.#frame !== void 0) cancelAnimationFrame(this.#frame);
144
147
  this.#frame = void 0;
@@ -32,13 +32,6 @@ var SonicNumber = class extends SonicValueElement {
32
32
  const number = this.#number;
33
33
  this.keepControl(number, signal);
34
34
  this.render();
35
- this.checkStyles(number, "number.css", [
36
- "cap",
37
- "glass",
38
- "pane",
39
- "well",
40
- "value"
41
- ]);
42
35
  this.bindGestures(number, signal, () => ({
43
36
  position: (event) => -event.clientY,
44
37
  travelPx: this.travelPx(number, "--_sonic-number-travel")