@xsynaptic/sonic-ui 0.10.4 → 0.13.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 (119) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +39 -15
  3. package/dist/default/define/toggle.js +7 -0
  4. package/dist/default/define.js +1 -0
  5. package/dist/default/elements/button.js +7 -1
  6. package/dist/default/elements/dial.js +7 -1
  7. package/dist/default/elements/display-element.js +5 -1
  8. package/dist/default/elements/envelope.js +6 -1
  9. package/dist/default/elements/meter.js +1 -1
  10. package/dist/default/elements/number.js +7 -1
  11. package/dist/default/elements/position-group.js +163 -0
  12. package/dist/default/elements/radio-group.js +34 -12
  13. package/dist/default/elements/segmented.js +5 -1
  14. package/dist/default/elements/slider.js +7 -1
  15. package/dist/default/elements/sonic-element.js +1 -1
  16. package/dist/default/elements/switch.js +34 -168
  17. package/dist/default/elements/toggle.js +193 -0
  18. package/dist/default/elements/value-element.js +3 -0
  19. package/dist/default/elements/value-gestures.js +7 -0
  20. package/dist/default/elements/wave-element.js +8 -1
  21. package/dist/default/elements/waveform.js +159 -13
  22. package/dist/default/elements/wavestrip.js +4 -0
  23. package/dist/default/elements/xy.js +7 -1
  24. package/dist/default/index.js +2 -1
  25. package/dist/default/lib/key-press.js +27 -0
  26. package/dist/default/lib/toggle-travel.js +19 -0
  27. package/dist/default/lib/zoom-gesture.js +19 -0
  28. package/dist/dev/define/toggle.d.ts +2 -0
  29. package/dist/dev/define/toggle.js +8 -0
  30. package/dist/dev/define/toggle.js.map +1 -0
  31. package/dist/dev/define.d.ts +3 -1
  32. package/dist/dev/define.js +1 -0
  33. package/dist/dev/elements/button.js +7 -1
  34. package/dist/dev/elements/button.js.map +1 -1
  35. package/dist/dev/elements/dial.js +7 -1
  36. package/dist/dev/elements/dial.js.map +1 -1
  37. package/dist/dev/elements/display-element.js +5 -1
  38. package/dist/dev/elements/display-element.js.map +1 -1
  39. package/dist/dev/elements/envelope.js +6 -1
  40. package/dist/dev/elements/envelope.js.map +1 -1
  41. package/dist/dev/elements/meter.js +1 -1
  42. package/dist/dev/elements/meter.js.map +1 -1
  43. package/dist/dev/elements/number.js +7 -1
  44. package/dist/dev/elements/number.js.map +1 -1
  45. package/dist/dev/elements/position-group.d.ts +38 -0
  46. package/dist/dev/elements/position-group.js +164 -0
  47. package/dist/dev/elements/position-group.js.map +1 -0
  48. package/dist/dev/elements/radio-group.d.ts +4 -0
  49. package/dist/dev/elements/radio-group.js +34 -12
  50. package/dist/dev/elements/radio-group.js.map +1 -1
  51. package/dist/dev/elements/segmented.js +5 -1
  52. package/dist/dev/elements/segmented.js.map +1 -1
  53. package/dist/dev/elements/slider.js +7 -1
  54. package/dist/dev/elements/slider.js.map +1 -1
  55. package/dist/dev/elements/sonic-element.d.ts +16 -1
  56. package/dist/dev/elements/sonic-element.js +27 -5
  57. package/dist/dev/elements/sonic-element.js.map +1 -1
  58. package/dist/dev/elements/switch.d.ts +4 -19
  59. package/dist/dev/elements/switch.js +34 -168
  60. package/dist/dev/elements/switch.js.map +1 -1
  61. package/dist/dev/elements/toggle.d.ts +21 -0
  62. package/dist/dev/elements/toggle.js +201 -0
  63. package/dist/dev/elements/toggle.js.map +1 -0
  64. package/dist/dev/elements/value-element.d.ts +1 -0
  65. package/dist/dev/elements/value-element.js +3 -0
  66. package/dist/dev/elements/value-element.js.map +1 -1
  67. package/dist/dev/elements/value-gestures.js +7 -0
  68. package/dist/dev/elements/value-gestures.js.map +1 -1
  69. package/dist/dev/elements/wave-element.js +8 -1
  70. package/dist/dev/elements/wave-element.js.map +1 -1
  71. package/dist/dev/elements/waveform.d.ts +11 -1
  72. package/dist/dev/elements/waveform.js +159 -13
  73. package/dist/dev/elements/waveform.js.map +1 -1
  74. package/dist/dev/elements/wavestrip.d.ts +1 -0
  75. package/dist/dev/elements/wavestrip.js +4 -0
  76. package/dist/dev/elements/wavestrip.js.map +1 -1
  77. package/dist/dev/elements/xy.js +7 -1
  78. package/dist/dev/elements/xy.js.map +1 -1
  79. package/dist/dev/index.d.ts +3 -2
  80. package/dist/dev/index.js +2 -1
  81. package/dist/dev/lib/key-press.js +28 -0
  82. package/dist/dev/lib/key-press.js.map +1 -0
  83. package/dist/dev/lib/toggle-travel.js +20 -0
  84. package/dist/dev/lib/toggle-travel.js.map +1 -0
  85. package/dist/dev/lib/zoom-gesture.js +20 -0
  86. package/dist/dev/lib/zoom-gesture.js.map +1 -0
  87. package/dist/styles/button.css +5 -3
  88. package/dist/styles/controls.css +1 -0
  89. package/dist/styles/dial.css +3 -1
  90. package/dist/styles/envelope.css +3 -1
  91. package/dist/styles/led.css +19 -14
  92. package/dist/styles/material/arc.css +21 -0
  93. package/dist/styles/material/bracket.css +31 -0
  94. package/dist/styles/material/cap.css +179 -0
  95. package/dist/styles/material/core.css +103 -0
  96. package/dist/styles/material/glass.css +62 -0
  97. package/dist/styles/material/groove.css +10 -0
  98. package/dist/styles/material/keycap.css +95 -0
  99. package/dist/styles/material/lens.css +11 -0
  100. package/dist/styles/material/pane.css +97 -0
  101. package/dist/styles/material/readout.css +87 -0
  102. package/dist/styles/material/scale.css +30 -0
  103. package/dist/styles/material/value.css +43 -0
  104. package/dist/styles/material/well.css +24 -0
  105. package/dist/styles/material.css +15 -745
  106. package/dist/styles/meter.css +10 -7
  107. package/dist/styles/number.css +3 -1
  108. package/dist/styles/panel.css +3 -1
  109. package/dist/styles/ring.css +3 -1
  110. package/dist/styles/screen.css +3 -1
  111. package/dist/styles/segmented.css +11 -5
  112. package/dist/styles/slider.css +3 -1
  113. package/dist/styles/spectrum.css +5 -3
  114. package/dist/styles/switch.css +9 -3
  115. package/dist/styles/toggle.css +483 -0
  116. package/dist/styles/waveform.css +3 -1
  117. package/dist/styles/wavestrip.css +3 -1
  118. package/dist/styles/xy.css +3 -1
  119. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @xsynaptic/sonic-ui
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - The material is split into sheets under `material/*.css`, so you can load only what your controls need; the README lists them. `material.css` and `controls.css` work as before, and sheets now load in any order.
8
+
9
+ New public surface: the material sheets, and the sublayers `sonic.material` and `sonic.control`. A rule written directly in `@layer sonic` now always wins over the control sheets.
10
+
11
+ - Waveform: `zoomable` opts in to zoom by ctrl or cmd wheel, two-finger pinch and the `+` and `-` keys, between `zoom-min` and `zoom-max` (20 and 280 by default). `sonic-zoom` fires for a gesture and never for a write to `zoom`. A second finger ends a scrub without a seek. Without `zoomable` nothing changes.
12
+
13
+ ## 0.12.0
14
+
15
+ ### Minor Changes
16
+
17
+ - Wave controls: `requestPeaks` may return one promise or an iterable of them, each repainting once however often it is returned, and its `PeaksRequest` type is exported. A write to `pending` or `peaks` inside the call no longer asks again for the same window, and `pending` ignores an unchanged list. The waveform now asks when its window moves, its data is written or a promise settles, not on every repaint. `<sonic-wavestrip>` gains `clientXOf(value)`, the viewport x where a value sits.
18
+
19
+ ## 0.11.0
20
+
21
+ ### Minor Changes
22
+
23
+ - - Breaking: lit colours take status names. `data-sonic-lit` values `alt`, `hot`, `clip` and `dim` are now `ok`, `warning`, `danger` and `idle`; `--sonic-lit-alt`, `--sonic-hot` and `--sonic-clip` are now `--sonic-lit-ok`, `--sonic-lit-warning` and `--sonic-lit-danger`. A meter's and a spectrum's hot and clip zones keep their names and draw in the warning and danger colours.
24
+ - Toggle: new `<sonic-toggle>`, a round cap sliding in a well, two or three positions or on/off, with lit, momentary and disabled positions and an optional LED or icon on the cap.
25
+ - Segmented control and switch: `data-sonic-disabled` on a position child disables that one position; arrows skip it, a press and the bat pass it by. A disabled switch position's label is drawn faint, which also dims the labels of a wholly disabled switch.
26
+ - Switch: a press on the rest label of a two-position switch holds its momentary position instead of latching it.
27
+ - Button and segmented control: a held Space shows the press in Firefox. `data-sonic-pressed` marks a part held by a key as well as by a pointer.
28
+ - Segmented control: on a skin whose ink is dark the chosen legend is drawn in the ink, so it reads on a light cap; the lit well still marks the choice.
29
+
3
30
  ## 0.10.4
4
31
 
5
32
  ### Patch Changes
package/README.md CHANGED
@@ -27,27 +27,51 @@ A display takes its signal as data: `spectrum.analyser = node`, or `spectrum.pus
27
27
 
28
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.
29
29
 
30
- ## Wave controls
30
+ ## Loading less
31
+
32
+ `controls.css` loads everything. To load less, import each control's sheet and the material sheets it needs, in any order:
31
33
 
32
- `<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 (`min`, `max`, `step`, `value`) and take their data as properties.
34
+ ```css
35
+ @import '@xsynaptic/sonic-ui/material/core.css';
36
+ @import '@xsynaptic/sonic-ui/material/lens.css';
37
+ @import '@xsynaptic/sonic-ui/material/groove.css';
38
+ @import '@xsynaptic/sonic-ui/meter.css';
39
+ ```
40
+
41
+ | Sheet | Needs from `material/` |
42
+ | --------------- | ----------------------------------------------- |
43
+ | `button.css` | core, cap, well, keycap |
44
+ | `dial.css` | core, cap, glass, readout, value, arc, scale |
45
+ | `envelope.css` | core, glass, pane, well, readout, bracket |
46
+ | `led.css` | core, lens |
47
+ | `meter.css` | core, lens, groove |
48
+ | `number.css` | core, cap, glass, pane, well, value |
49
+ | `panel.css` | core |
50
+ | `ring.css` | core, arc |
51
+ | `screen.css` | core, glass, pane, well |
52
+ | `segmented.css` | core, cap, well, keycap |
53
+ | `slider.css` | core, cap, glass, readout, value, groove, scale |
54
+ | `spectrum.css` | core, glass, pane, well |
55
+ | `switch.css` | core, cap |
56
+ | `toggle.css` | core, cap, well, keycap |
57
+ | `waveform.css` | core, cap, glass, pane, well, readout, value |
58
+ | `wavestrip.css` | core, cap, glass, pane, well, readout, value |
59
+ | `xy.css` | core, cap, glass, pane, well, readout, bracket |
60
+
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
33
64
 
34
- | | Both | Wave strip | Waveform |
35
- | --- | --- | --- | --- |
36
- | Attributes | `fill`, `readout`, `disabled`, `dimmed` | `cancellable` | `playing`, `playback-rate`, `zoom`, `pending-delay`, `reduced-motion` |
37
- | Properties | `markers`, `currentMarker`, `formatValue`, `formatSpokenValue`, `repaint()` | `peaks`, `buffered` | `peaks`, `pending`, `readTime`, `requestPeaks` |
38
- | Gesture, read only | `dragging`, `revealed`, `cancelling`, `pointerType` | `hoverValue` | |
39
- | Events, all bubbling | `input`, `change`, `sonic-reveal`, `sonic-marker` | `sonic-hover` | |
40
- | States | `dragging`, `revealed`, `cancelling`, `editing`, `empty` | | `pending` |
41
- | Hook classes | | `sonic-wavestrip`, `-canvas`, `-markers`, `-marker`, `-region`, `-readout`, `-entry` | `sonic-waveform`, `-canvas`, `-scrim`, `-playhead`, `-ghost`, `-label`, `-readout`, `-entry` |
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.
42
66
 
43
- The events carry no detail; read the properties when one fires. `sonic-reveal` reports `revealed` changing, and a drag crossing the cancel zone reports through it or through `input`.
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.
44
68
 
45
- A marker is `{ start, end?, label?, kind?, dimmed? }`; `markers` reads back sorted by `start`. The strip's `.sonic-wavestrip-markers` holds one part per marker in that order, a `.sonic-wavestrip-marker` dot or, with an `end`, a `.sonic-wavestrip-region`, so a dot's box gives the row it was laid on.
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.
46
70
 
47
- Marker labels are drawn parts and hidden from assistive technology. To speak the current marker with the time, fold `currentMarker` into `formatSpokenValue`; to announce it during playback, feed a live region of your own from `sonic-marker`.
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.
48
72
 
49
- `requestPeaks(fromSeconds, toSeconds)` is called whenever the window the waveform wants moves, often every frame, so it should return at once for a span already loaded or on its way. Return a promise and the waveform repaints when it settles, resolved or rejected. `peaks` holds no record of what has loaded, so `pending` stays yours: list the spans you are fetching, and clear them as each lands.
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.
50
74
 
51
- Sizes are ratios of the control's own size (`--sonic-wavestrip-size`, `--sonic-waveform-size`). Where a `fill` control should keep an absolute length, these override their ratios: `--sonic-wavestrip-marker-size`, `--sonic-wavestrip-groove-size`, `--sonic-waveform-label-font-size`, `--sonic-waveform-label-inset-inline` and `--sonic-waveform-label-inset-block`. `--sonic-waveform-label-line-height` defaults to 1.
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.
52
76
 
53
77
  Targets current Chrome and Edge, Safari 26 and Firefox 147. MIT licensed.
@@ -0,0 +1,7 @@
1
+ import { defineOnce } from "../elements/define-once.js";
2
+ import { SonicToggle } from "../elements/toggle.js";
3
+
4
+ //#region src/define/toggle.ts
5
+ defineOnce("sonic-toggle", SonicToggle);
6
+
7
+ //#endregion
@@ -8,6 +8,7 @@ import "./define/slider.js";
8
8
  import "./define/spectrum.js";
9
9
  import "./define/split.js";
10
10
  import "./define/switch.js";
11
+ import "./define/toggle.js";
11
12
  import "./define/waveform.js";
12
13
  import "./define/wavestrip.js";
13
14
  import "./define/xy.js";
@@ -2,6 +2,7 @@ import { writeAttribute } from "../lib/write-attribute.js";
2
2
  import { SonicFormElement } from "./form-element.js";
3
3
  import { copyNode } from "../lib/copy-node.js";
4
4
  import { bindHold } from "../lib/hold.js";
5
+ import { bindKeyPress } from "../lib/key-press.js";
5
6
  import { placeChildren, requireChild, template } from "../lib/render.js";
6
7
  import { mirrorChildren } from "../lib/mirror-children.js";
7
8
  import { capturePointer } from "../lib/pointer-drag.js";
@@ -113,7 +114,11 @@ var SonicButton = class extends SonicFormElement {
113
114
  }
114
115
  }, signal);
115
116
  this.render();
116
- this.checkStyles(button, "button.css");
117
+ this.checkStyles(button, "button.css", [
118
+ "cap",
119
+ "well",
120
+ "keycap"
121
+ ]);
117
122
  button.addEventListener("click", (event) => {
118
123
  if (this.#isSoftDisabled()) {
119
124
  event.preventDefault();
@@ -125,6 +130,7 @@ var SonicButton = class extends SonicFormElement {
125
130
  this.dispatchEvent(new Event("change", { bubbles: true }));
126
131
  }, { signal });
127
132
  this.#bindMomentary(button, signal);
133
+ bindKeyPress(button, signal);
128
134
  }
129
135
  focusTarget() {
130
136
  return this.#button;
@@ -64,7 +64,13 @@ var SonicDial = class extends SonicValueElement {
64
64
  marks: requireChild(dial, ".sonic-dial-scale", HTMLDivElement)
65
65
  }, signal);
66
66
  this.render();
67
- this.checkStyles(dial, "dial.css");
67
+ this.checkStyles(dial, "dial.css", [
68
+ "cap",
69
+ "readout",
70
+ "value",
71
+ ["glass", ".sonic-dial-readout"],
72
+ ["arc", ".sonic-dial-ring"]
73
+ ]);
68
74
  this.bindGestures(dial, signal, () => ({
69
75
  position: (event) => -event.clientY,
70
76
  travelPx: this.travelPx(dial, "--_sonic-dial-travel")
@@ -66,7 +66,11 @@ var SonicDisplayElement = class extends SonicElement {
66
66
  },
67
67
  signal
68
68
  });
69
- this.checkStyles(control, this.sheet);
69
+ this.checkStyles(control, this.sheet, [
70
+ "glass",
71
+ "pane",
72
+ "well"
73
+ ]);
70
74
  this.#syncFill = bindFill(control, this.sizeProperty, signal);
71
75
  this.#syncFill(this.fill);
72
76
  }
@@ -170,7 +170,12 @@ var SonicEnvelope = class extends SonicElement {
170
170
  this.#signal = signal;
171
171
  this.keepControl(envelope, signal);
172
172
  this.#bind();
173
- this.checkStyles(envelope, "envelope.css");
173
+ this.checkStyles(envelope, "envelope.css", [
174
+ "glass",
175
+ "pane",
176
+ "well",
177
+ "readout"
178
+ ]);
174
179
  this.#bindPointer(envelope, signal);
175
180
  signal.addEventListener("abort", () => {
176
181
  this.#signal = void 0;
@@ -138,7 +138,7 @@ var SonicMeter = class extends SonicElement {
138
138
  this.#renderSegments();
139
139
  this.#render(now);
140
140
  this.#schedule(now);
141
- this.checkStyles(this.#meter, "meter.css");
141
+ this.checkStyles(this.#meter, "meter.css", ["lens", "groove"]);
142
142
  signal.addEventListener("abort", () => {
143
143
  if (this.#frame !== void 0) cancelAnimationFrame(this.#frame);
144
144
  this.#frame = void 0;
@@ -32,7 +32,13 @@ 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");
35
+ this.checkStyles(number, "number.css", [
36
+ "cap",
37
+ "glass",
38
+ "pane",
39
+ "well",
40
+ "value"
41
+ ]);
36
42
  this.bindGestures(number, signal, () => ({
37
43
  position: (event) => -event.clientY,
38
44
  travelPx: this.travelPx(number, "--_sonic-number-travel")
@@ -0,0 +1,163 @@
1
+ import { writeAttribute } from "../lib/write-attribute.js";
2
+ import { bindHold } from "../lib/hold.js";
3
+ import { SonicRadioGroupElement, optionValue } from "./radio-group.js";
4
+
5
+ //#region src/elements/position-group.ts
6
+ var SonicPositionGroupElement = class extends SonicRadioGroupElement {
7
+ static observedAttributes = [
8
+ ...SonicRadioGroupElement.observedAttributes,
9
+ "checked",
10
+ "orientation"
11
+ ];
12
+ get checked() {
13
+ return this.#isDirty ? this.#checked : this.defaultChecked;
14
+ }
15
+ set checked(isChecked) {
16
+ this.#isDirty = true;
17
+ this.#checked = isChecked === true;
18
+ this.render();
19
+ }
20
+ get defaultChecked() {
21
+ return this.hasAttribute("checked");
22
+ }
23
+ set defaultChecked(isChecked) {
24
+ this.reflect("checked", isChecked);
25
+ }
26
+ get orientation() {
27
+ const orientation = this.getAttribute("orientation");
28
+ return orientation === "horizontal" || orientation === "vertical" ? orientation : this.defaultOrientation;
29
+ }
30
+ set orientation(direction) {
31
+ this.reflect("orientation", direction);
32
+ }
33
+ get value() {
34
+ const value = super.value;
35
+ return value === "" && this.isBare() ? "on" : value;
36
+ }
37
+ set value(next) {
38
+ super.value = next;
39
+ }
40
+ #checked = false;
41
+ #holding;
42
+ #isDirty = false;
43
+ attributeChangedCallback(name) {
44
+ if (name === "checked") this.#isDirty = false;
45
+ if (this.isDisabled()) this.#holding?.release();
46
+ super.attributeChangedCallback(name);
47
+ }
48
+ formResetCallback() {
49
+ this.#isDirty = false;
50
+ super.formResetCallback();
51
+ }
52
+ activate() {
53
+ if (this.isBare()) this.bare.click();
54
+ else super.activate();
55
+ }
56
+ along(event) {
57
+ return this.orientation === "vertical" ? event.clientY : event.clientX;
58
+ }
59
+ checkedIndex(options) {
60
+ return options.findIndex((option) => optionValue(option) === this.value);
61
+ }
62
+ chooseByClick(option) {
63
+ super.chooseByClick(option);
64
+ if (this.isMomentary(option)) this.springBack();
65
+ }
66
+ chooseByKey(option, event) {
67
+ if (!this.isMomentary(option)) {
68
+ super.chooseByKey(option, event);
69
+ return;
70
+ }
71
+ if (event.repeat) return;
72
+ this.hold(option, event.key);
73
+ option.focus();
74
+ }
75
+ connect(signal) {
76
+ this.upgradeProperties("defaultChecked", "checked", "orientation");
77
+ super.connect(signal);
78
+ this.#holding = bindHold(this.group, {
79
+ canHold: () => !this.isDisabled(),
80
+ leave: "focusout",
81
+ onHold: () => {
82
+ this.toggleState("held", true);
83
+ },
84
+ onRelease: () => {
85
+ this.toggleState("held", false);
86
+ this.springBack();
87
+ }
88
+ }, signal);
89
+ }
90
+ focusTarget() {
91
+ return this.isBare() ? this.bare : super.focusTarget();
92
+ }
93
+ hold(option, by) {
94
+ if (this.#holding?.hold(by) === true) this.select(option);
95
+ }
96
+ isBare() {
97
+ return this.options().length === 0;
98
+ }
99
+ isMomentary(option) {
100
+ const options = this.options();
101
+ return (option === options[0] || option === options.at(-1)) && options.length > 1 && option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-momentary") === true;
102
+ }
103
+ isWrapping() {
104
+ return false;
105
+ }
106
+ pressTarget(option) {
107
+ const options = this.options();
108
+ const index = this.checkedIndex(options);
109
+ return (options.length === 2 && index >= 0 ? options[1 - index] : void 0) ?? option;
110
+ }
111
+ releaseHold() {
112
+ this.#holding?.release();
113
+ }
114
+ render() {
115
+ if (!this.isBound()) return;
116
+ const group = this.group;
117
+ const count = this.options().length;
118
+ if (count === 0) {
119
+ this.#renderBare();
120
+ return;
121
+ }
122
+ this.bare.remove();
123
+ writeAttribute(group, "role", "radiogroup");
124
+ writeAttribute(group, "data-sonic-position-count", String(count));
125
+ super.render();
126
+ }
127
+ restoreState(state) {
128
+ if (this.isBare()) this.checked = state === "true";
129
+ else super.restoreState(state);
130
+ }
131
+ setChecked(isChecked) {
132
+ if (isChecked === this.checked) return;
133
+ this.checked = isChecked;
134
+ this.dispatchEvent(new Event("change", { bubbles: true }));
135
+ }
136
+ springBack() {
137
+ const options = this.options();
138
+ const index = this.checkedIndex(options);
139
+ const held = options[index];
140
+ if (!held || !this.isMomentary(held)) return;
141
+ const rest = options[index === 0 ? 1 : index - 1];
142
+ if (!rest) return;
143
+ const isFocused = this.group.contains(this.ownerDocument.activeElement);
144
+ this.select(rest);
145
+ if (isFocused) rest.focus();
146
+ }
147
+ #renderBare() {
148
+ const group = this.group;
149
+ const bare = this.bare;
150
+ const isChecked = this.checked;
151
+ writeAttribute(group, "role", void 0);
152
+ writeAttribute(group, "data-sonic-position-count", void 0);
153
+ if (bare.parentElement !== group) group.append(bare);
154
+ writeAttribute(bare, "disabled", this.isDisabled() ? "" : void 0);
155
+ writeAttribute(bare, "aria-checked", String(isChecked));
156
+ this.forwardNaming(group, false);
157
+ this.forwardNaming(bare, true);
158
+ this.writeFormValue(isChecked ? this.value : null, String(isChecked));
159
+ }
160
+ };
161
+
162
+ //#endregion
163
+ export { SonicPositionGroupElement };
@@ -1,6 +1,8 @@
1
+ import { clamp } from "../lib/math.js";
1
2
  import { writeAttribute } from "../lib/write-attribute.js";
2
3
  import { SonicFormElement } from "./form-element.js";
3
4
  import { copyNode } from "../lib/copy-node.js";
5
+ import { bindKeyPress } from "../lib/key-press.js";
4
6
  import { placeChildren } from "../lib/render.js";
5
7
  import { mirrorChildren } from "../lib/mirror-children.js";
6
8
  import { bindDrag } from "../lib/pointer-drag.js";
@@ -52,9 +54,9 @@ var SonicRadioGroupElement = class extends SonicFormElement {
52
54
  mirrorChildren(this, {
53
55
  control: group,
54
56
  copy: copyNode,
55
- isCopied: (child) => child instanceof Element && child.matches("[data-sonic-value]"),
57
+ isCopied: (child) => this.isMirrored(child),
56
58
  place: (copies) => {
57
- this.#copyOptions(copies);
59
+ this.placeCopies(copies);
58
60
  }
59
61
  }, signal);
60
62
  group.addEventListener("click", (event) => {
@@ -63,6 +65,7 @@ var SonicRadioGroupElement = class extends SonicFormElement {
63
65
  if (option) this.chooseByClick(option);
64
66
  }, { signal });
65
67
  this.#bindPress(group, signal);
68
+ bindKeyPress(group, signal);
66
69
  group.addEventListener("keydown", (event) => {
67
70
  if (event.defaultPrevented) return;
68
71
  const option = this.optionOf(event.target);
@@ -75,6 +78,12 @@ var SonicRadioGroupElement = class extends SonicFormElement {
75
78
  focusTarget() {
76
79
  return this.options().find((option) => option.tabIndex === 0);
77
80
  }
81
+ isMirrored(child) {
82
+ return child instanceof Element && child.matches("[data-sonic-value]");
83
+ }
84
+ isOptionDisabled(option) {
85
+ return option.querySelector("[data-sonic-value]")?.hasAttribute("data-sonic-disabled") === true;
86
+ }
78
87
  isWrapping() {
79
88
  return true;
80
89
  }
@@ -86,6 +95,9 @@ var SonicRadioGroupElement = class extends SonicFormElement {
86
95
  options() {
87
96
  return [...this.group.children].filter((child) => child instanceof HTMLButtonElement && child.getAttribute("role") === "radio");
88
97
  }
98
+ placeCopies(copies) {
99
+ this.#copyOptions(copies);
100
+ }
89
101
  releaseTarget(option) {
90
102
  return option;
91
103
  }
@@ -93,10 +105,10 @@ var SonicRadioGroupElement = class extends SonicFormElement {
93
105
  if (!this.isBound()) return;
94
106
  const options = this.options();
95
107
  const checked = options.find((option) => optionValue(option) === this.#value);
96
- const focusable = checked ?? options[0];
97
- const isDisabled = this.isDisabled();
108
+ const enabled = this.isDisabled() ? [] : options.filter((option) => !this.isOptionDisabled(option));
109
+ const focusable = enabled.find((option) => option === checked) ?? enabled[0];
98
110
  for (const option of options) {
99
- writeAttribute(option, "disabled", isDisabled ? "" : void 0);
111
+ writeAttribute(option, "disabled", enabled.includes(option) ? void 0 : "");
100
112
  writeAttribute(option, "aria-checked", String(option === checked));
101
113
  writeAttribute(option, "tabindex", option === focusable ? "0" : "-1");
102
114
  }
@@ -112,11 +124,22 @@ var SonicRadioGroupElement = class extends SonicFormElement {
112
124
  this.value = next;
113
125
  this.dispatchEvent(new Event("change", { bubbles: true }));
114
126
  }
127
+ stepFrom(from, step) {
128
+ const options = this.options();
129
+ let index = from;
130
+ for (const _ of options) {
131
+ const next = index + step;
132
+ index = this.isWrapping() ? (next + options.length) % options.length : clamp(next, 0, options.length - 1);
133
+ const option = options[index];
134
+ if (option && !this.isOptionDisabled(option)) return option;
135
+ }
136
+ }
115
137
  #bindPress(group, signal) {
116
138
  bindDrag(group, {
117
139
  grab: (event) => {
118
140
  const option = this.optionOf(event.target);
119
- if (!option || this.isDisabled() || this.claimPress(option, event)) return;
141
+ if (!option || this.isDisabled() || this.isOptionDisabled(option)) return;
142
+ if (this.claimPress(option, event)) return;
120
143
  const boxes = this.#optionBoxes();
121
144
  this.#markPressed(option);
122
145
  return {
@@ -159,19 +182,18 @@ var SonicRadioGroupElement = class extends SonicFormElement {
159
182
  }
160
183
  #keyTarget(option, key) {
161
184
  const options = this.options();
162
- if (key === "Home") return options[0];
163
- if (key === "End") return options.at(-1);
185
+ const isEnabled = (other) => !this.isOptionDisabled(other);
186
+ if (key === "Home") return options.find((other) => isEnabled(other));
187
+ if (key === "End") return options.findLast((other) => isEnabled(other));
164
188
  const step = keySteps.get(key);
165
189
  if (step === void 0) return void 0;
166
- const index = options.indexOf(option) + step;
167
- if (this.isWrapping()) return options[(index + options.length) % options.length];
168
- return options[Math.min(Math.max(index, 0), options.length - 1)];
190
+ return this.stepFrom(options.indexOf(option), step) ?? option;
169
191
  }
170
192
  #markPressed(pressed) {
171
193
  for (const option of this.options()) option.toggleAttribute("data-sonic-pressed", option === pressed);
172
194
  }
173
195
  #optionBoxes() {
174
- return this.options().map((option) => [option, option.getBoundingClientRect()]);
196
+ return this.options().filter((option) => !this.isOptionDisabled(option)).map((option) => [option, option.getBoundingClientRect()]);
175
197
  }
176
198
  #refocus(focused, value) {
177
199
  const options = this.options();
@@ -12,7 +12,11 @@ var SonicSegmented = class extends SonicRadioGroupElement {
12
12
  group = renderSegmented();
13
13
  connect(signal) {
14
14
  super.connect(signal);
15
- this.checkStyles(this.group, "segmented.css");
15
+ this.checkStyles(this.group, "segmented.css", [
16
+ "well",
17
+ "keycap",
18
+ ["cap", ".sonic-segmented-option"]
19
+ ]);
16
20
  }
17
21
  renderOption() {
18
22
  return renderOption();
@@ -114,7 +114,13 @@ var SonicSlider = class extends SonicValueElement {
114
114
  }
115
115
  });
116
116
  this.render();
117
- this.checkStyles(slider, "slider.css");
117
+ this.checkStyles(slider, "slider.css", [
118
+ "cap",
119
+ "readout",
120
+ "value",
121
+ "groove",
122
+ ["glass", ".sonic-slider-readout"]
123
+ ]);
118
124
  this.bindGestures(slider, signal, (event) => {
119
125
  const axis = this.#axis(slider, cap);
120
126
  if (event.target instanceof Node && cap.contains(event.target)) return axis;
@@ -58,7 +58,7 @@ var SonicElement = class extends HTMLElement {
58
58
  }
59
59
  if (!this.isDisabled() && !this.#focused()) target.focus(options);
60
60
  }
61
- checkStyles(control, sheet) {}
61
+ checkStyles(control, sheet, material = []) {}
62
62
  focusTarget() {}
63
63
  forwardNaming(target, isNamed) {
64
64
  const labels = isNamed ? this.namingLabels() : [];