@xsynaptic/sonic-ui 0.2.0 → 0.4.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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/default/define/number.js +7 -0
  3. package/dist/default/define.js +1 -0
  4. package/dist/default/elements/dial.js +24 -10
  5. package/dist/default/elements/key.js +29 -4
  6. package/dist/default/elements/meter.js +63 -9
  7. package/dist/default/elements/number.js +46 -0
  8. package/dist/default/elements/range-element.js +172 -36
  9. package/dist/default/elements/slider.js +63 -3
  10. package/dist/default/index.js +2 -1
  11. package/dist/default/lib/detent.js +17 -0
  12. package/dist/default/lib/math.js +16 -0
  13. package/dist/default/lib/number-list.js +14 -0
  14. package/dist/default/lib/taper.js +17 -4
  15. package/dist/dev/define/number.d.ts +2 -0
  16. package/dist/dev/define/number.js +8 -0
  17. package/dist/dev/define/number.js.map +1 -0
  18. package/dist/dev/define.d.ts +3 -1
  19. package/dist/dev/define.js +1 -0
  20. package/dist/dev/elements/dial.d.ts +3 -0
  21. package/dist/dev/elements/dial.js +24 -10
  22. package/dist/dev/elements/dial.js.map +1 -1
  23. package/dist/dev/elements/key.d.ts +4 -0
  24. package/dist/dev/elements/key.js +29 -4
  25. package/dist/dev/elements/key.js.map +1 -1
  26. package/dist/dev/elements/meter.d.ts +6 -0
  27. package/dist/dev/elements/meter.js +63 -9
  28. package/dist/dev/elements/meter.js.map +1 -1
  29. package/dist/dev/elements/number.d.ts +16 -0
  30. package/dist/dev/elements/number.js +47 -0
  31. package/dist/dev/elements/number.js.map +1 -0
  32. package/dist/dev/elements/range-element.d.ts +12 -0
  33. package/dist/dev/elements/range-element.js +172 -36
  34. package/dist/dev/elements/range-element.js.map +1 -1
  35. package/dist/dev/elements/slider.d.ts +8 -0
  36. package/dist/dev/elements/slider.js +63 -3
  37. package/dist/dev/elements/slider.js.map +1 -1
  38. package/dist/dev/index.d.ts +2 -1
  39. package/dist/dev/index.js +2 -1
  40. package/dist/dev/lib/detent.js +18 -0
  41. package/dist/dev/lib/detent.js.map +1 -0
  42. package/dist/dev/lib/math.js +17 -0
  43. package/dist/dev/lib/math.js.map +1 -0
  44. package/dist/dev/lib/number-list.js +15 -0
  45. package/dist/dev/lib/number-list.js.map +1 -0
  46. package/dist/dev/lib/read-px-property.js.map +1 -1
  47. package/dist/dev/lib/taper.js +17 -4
  48. package/dist/dev/lib/taper.js.map +1 -1
  49. package/dist/styles/controls.css +2 -0
  50. package/dist/styles/dial.css +80 -6
  51. package/dist/styles/key.css +96 -11
  52. package/dist/styles/led.css +82 -0
  53. package/dist/styles/material.css +150 -20
  54. package/dist/styles/meter.css +120 -8
  55. package/dist/styles/number.css +81 -0
  56. package/dist/styles/screen.css +6 -39
  57. package/dist/styles/segmented.css +23 -7
  58. package/dist/styles/slider.css +143 -30
  59. package/package.json +4 -2
@@ -0,0 +1,82 @@
1
+ @property --_sonic-led-size {
2
+ syntax: '<length>';
3
+ inherits: true;
4
+ initial-value: 8px;
5
+ }
6
+
7
+ @layer sonic {
8
+ .sonic-led {
9
+ --_sonic-led-size: var(--sonic-led-size, 0.5rem);
10
+ --_sonic-led-lens-ratio: var(--sonic-led-lens-ratio, 0.625);
11
+ --_sonic-lit-alt: var(--sonic-lit-alt, oklch(79% 0.2 150deg));
12
+ --_sonic-unit: calc(var(--_sonic-led-size) / 8);
13
+ --_sonic-led-bezel: color-mix(in oklab, var(--_sonic-unlit) 55%, #000);
14
+
15
+ position: relative;
16
+ display: inline-block;
17
+ flex-shrink: 0;
18
+ inline-size: var(--_sonic-led-size);
19
+ block-size: var(--_sonic-led-size);
20
+ background: var(--_sonic-led-bezel);
21
+ border-radius: 50%;
22
+
23
+ &::after {
24
+ position: absolute;
25
+ inset: calc(var(--_sonic-led-size) * (1 - var(--_sonic-led-lens-ratio)) / 2);
26
+ content: '';
27
+ background: var(--_sonic-lens);
28
+ border-radius: inherit;
29
+ }
30
+
31
+ @media (forced-colors: active) {
32
+ --_sonic-lit-alt: Mark;
33
+
34
+ forced-color-adjust: none;
35
+ background: none;
36
+
37
+ &::after {
38
+ background: GrayText;
39
+ box-shadow: 0 0 0 1px CanvasText;
40
+ }
41
+ }
42
+ }
43
+
44
+ .sonic-led[data-sonic-lit='alt'] {
45
+ --_sonic-lit: var(--_sonic-lit-alt);
46
+ }
47
+
48
+ .sonic-led:is(
49
+ [data-sonic-lit],
50
+ :where(
51
+ .sonic-key[aria-pressed='true'],
52
+ [momentary]:state(pressed) > .sonic-key,
53
+ .sonic-segmented-segment[aria-checked='true']
54
+ )
55
+ *
56
+ ) {
57
+ background:
58
+ radial-gradient(
59
+ closest-side,
60
+ color-mix(in oklab, var(--_sonic-lit) 60%, transparent)
61
+ calc(var(--_sonic-led-lens-ratio) * 100%),
62
+ transparent
63
+ ),
64
+ var(--_sonic-led-bezel);
65
+
66
+ &::after {
67
+ background: radial-gradient(
68
+ closest-side,
69
+ color-mix(in oklab, var(--_sonic-lit) 55%, #fff),
70
+ var(--_sonic-lit)
71
+ );
72
+ }
73
+
74
+ @media (forced-colors: active) {
75
+ background: none;
76
+
77
+ &::after {
78
+ background: var(--_sonic-lit);
79
+ }
80
+ }
81
+ }
82
+ }
@@ -1,10 +1,24 @@
1
+ @property --_sonic-readout-size {
2
+ syntax: '<length>';
3
+ inherits: true;
4
+ initial-value: 24px;
5
+ }
6
+
7
+ @property --_sonic-detent-zone {
8
+ syntax: '<length>';
9
+ inherits: true;
10
+ initial-value: 8px;
11
+ }
12
+
1
13
  @layer sonic {
2
14
  /* Resolved per control rather than on `:root`, so each follows its own cap and unit */
3
15
  :is(
4
16
  .sonic-dial,
5
17
  .sonic-dial-readout,
6
18
  .sonic-key,
19
+ .sonic-led,
7
20
  .sonic-meter,
21
+ .sonic-number,
8
22
  .sonic-screen,
9
23
  .sonic-segmented,
10
24
  .sonic-slider,
@@ -19,21 +33,36 @@
19
33
  --_sonic-cast-y: calc(cos(var(--_sonic-light-tilt)) * var(--_sonic-unit));
20
34
  }
21
35
 
22
- :is(.sonic-dial, .sonic-key, .sonic-meter, .sonic-segmented-segment, .sonic-slider) {
36
+ :is(
37
+ .sonic-dial,
38
+ .sonic-key,
39
+ .sonic-led,
40
+ .sonic-meter,
41
+ .sonic-number,
42
+ .sonic-segmented-segment,
43
+ .sonic-slider
44
+ ) {
23
45
  --_sonic-lit: var(--sonic-lit, oklch(76.9% 0.165 70deg));
24
46
  }
25
47
 
48
+ :is(.sonic-led, .sonic-meter) {
49
+ --_sonic-lens:
50
+ linear-gradient(var(--_sonic-light-fall), rgb(255 255 255 / 10%), transparent 60%),
51
+ color-mix(in oklab, var(--_sonic-unlit) 60%, #000);
52
+ }
53
+
26
54
  .sonic-meter {
27
55
  --_sonic-hot: var(--sonic-hot, oklch(70.5% 0.187 48deg));
28
56
  --_sonic-clip: var(--sonic-clip, oklch(63.7% 0.208 25deg));
29
57
  }
30
58
 
31
- :is(.sonic-dial, .sonic-key, .sonic-segmented-segment, .sonic-slider) {
59
+ :is(.sonic-dial, .sonic-key, .sonic-number, .sonic-segmented-segment, .sonic-slider) {
32
60
  --_sonic-cap: var(--sonic-cap, oklch(37% 0.012 286deg));
33
61
  --_sonic-cap-hover: var(--sonic-cap-hover, oklch(44.2% 0.015 286deg));
34
62
  --_sonic-focus: var(--sonic-focus, oklch(90% 0.23 128deg));
35
63
  --_sonic-ink: var(--sonic-mark, color-mix(in oklab, var(--_sonic-cap) 30%, #fff));
36
64
  --_sonic-etch: color-mix(in oklab, var(--_sonic-ink) 30%, transparent);
65
+ --_sonic-ink-lift: color-mix(in oklab, var(--_sonic-ink) 60%, #fff);
37
66
  --_sonic-lit-disabled: color-mix(in oklab, var(--_sonic-ink) 25%, var(--_sonic-unlit));
38
67
  --_sonic-skirt: linear-gradient(
39
68
  var(--_sonic-light-fall),
@@ -70,24 +99,76 @@
70
99
  color-mix(in oklab, var(--_sonic-focus) 60%, transparent);
71
100
  }
72
101
 
102
+ /* A gesture distance, absolute like the travel */
103
+ :is(.sonic-dial, .sonic-number, .sonic-slider) {
104
+ --_sonic-detent-zone: var(--sonic-detent-zone, 8px);
105
+ }
106
+
73
107
  :is(.sonic-dial, .sonic-slider) {
108
+ /* On the control, since the readout's font size reads it */
109
+ --_sonic-readout-size: var(--sonic-readout-size, 1.5rem);
74
110
  --_sonic-lit-off: var(
75
111
  --sonic-dimmed,
76
112
  color-mix(in oklab, var(--_sonic-lit) 50%, var(--_sonic-unlit))
77
113
  );
78
114
  }
79
115
 
80
- :is(.sonic-dial-readout, .sonic-screen, .sonic-slider-readout) {
116
+ :is(.sonic-dial-readout, .sonic-number, .sonic-screen, .sonic-slider-readout) {
81
117
  --_sonic-glass: var(--sonic-glass, color-mix(in oklab, var(--_sonic-unlit) 45%, #000));
118
+ --_sonic-glass-fill: radial-gradient(
119
+ closest-side,
120
+ color-mix(in oklab, var(--_sonic-glass) 88%, #fff),
121
+ var(--_sonic-glass)
122
+ )
123
+ var(--_sonic-glass);
124
+
125
+ /* Not the cap's ink, which a skin may darken */
126
+ --_sonic-glass-text: var(
127
+ --sonic-glass-text,
128
+ color-mix(in oklab, var(--_sonic-glass) 20%, #fff)
129
+ );
130
+ }
131
+
132
+ /* A definite cell, so percentage-sized content fills the glass; a canvas sized to its own box would feed back */
133
+ :is(.sonic-number, .sonic-screen) {
134
+ position: relative;
135
+ box-sizing: border-box;
136
+ display: grid;
137
+ flex-shrink: 0;
138
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
139
+ place-items: center;
140
+ font-family: var(--sonic-glass-font);
141
+ color: var(--_sonic-glass-text);
142
+
143
+ /* A double click aimed at a neighbouring control would highlight it */
144
+ user-select: none;
145
+ background: var(--_sonic-glass-fill);
146
+
147
+ &::after {
148
+ position: absolute;
149
+ inset: 0;
150
+ pointer-events: none;
151
+ content: '';
152
+ background: linear-gradient(var(--_sonic-light-fall), rgb(255 255 255 / 7%), transparent 45%);
153
+ border-radius: inherit;
154
+ box-shadow:
155
+ var(--_sonic-well),
156
+ inset 0 0 calc(3 * var(--_sonic-unit)) rgb(0 0 0 / 45%);
157
+ }
82
158
  }
83
159
 
84
- :is(.sonic-key, .sonic-screen, .sonic-segmented) {
160
+ :is(.sonic-key, .sonic-number, .sonic-screen, .sonic-segmented) {
85
161
  --_sonic-well:
86
162
  inset var(--_sonic-cast-x) var(--_sonic-cast-y) calc(2 * var(--_sonic-unit)) rgb(0 0 0 / 60%),
87
163
  calc(0.5 * var(--_sonic-cast-x)) calc(0.5 * var(--_sonic-cast-y)) 0 rgb(255 255 255 / 6%);
88
164
  }
89
165
 
90
166
  :is(.sonic-key, .sonic-segmented) {
167
+ --_sonic-well-floor: linear-gradient(
168
+ var(--_sonic-light-fall),
169
+ color-mix(in oklab, var(--_sonic-unlit) 55%, #000),
170
+ color-mix(in oklab, var(--_sonic-unlit) 85%, #000)
171
+ );
91
172
  --_sonic-engraving: drop-shadow(
92
173
  calc(-0.4 * var(--_sonic-cast-x)) calc(-0.4 * var(--_sonic-cast-y))
93
174
  calc(0.4 * var(--_sonic-unit)) rgb(0 0 0 / 30%)
@@ -97,6 +178,12 @@
97
178
  );
98
179
  }
99
180
 
181
+ /* On the segment rather than the strip, where forced colours clear the drop shade */
182
+ :is(.sonic-key, .sonic-segmented-segment) {
183
+ --_sonic-press-drop: calc(0.5 * var(--_sonic-cast-x)) calc(0.5 * var(--_sonic-cast-y))
184
+ var(--_sonic-unit) var(--_sonic-drop-shade);
185
+ }
186
+
100
187
  /* The well's full blur would fill a slot this narrow */
101
188
  :is(.sonic-meter, .sonic-slider) {
102
189
  --_sonic-groove:
@@ -115,7 +202,13 @@
115
202
  }
116
203
  }
117
204
 
118
- :is(.sonic-dial, .sonic-key, .sonic-segmented-segment, .sonic-slider):focus-visible {
205
+ :is(
206
+ .sonic-dial,
207
+ .sonic-key,
208
+ .sonic-number,
209
+ .sonic-segmented-segment,
210
+ .sonic-slider
211
+ ):focus-visible {
119
212
  --_sonic-cap: var(--_sonic-cap-hover);
120
213
  --_sonic-focus-shadow: var(--_sonic-focus-glow);
121
214
 
@@ -124,13 +217,12 @@
124
217
  }
125
218
 
126
219
  /* Keeps the glow while focus is in the entry; its own rule, since an engine without `:state()` drops a whole selector list */
127
- :state(editing) > :is(.sonic-dial, .sonic-slider) {
220
+ :state(editing) > :is(.sonic-dial, .sonic-number, .sonic-slider) {
128
221
  --_sonic-cap: var(--_sonic-cap-hover);
129
222
  --_sonic-focus-shadow: var(--_sonic-focus-glow);
130
223
  }
131
224
 
132
225
  :is(.sonic-dial-readout, .sonic-slider-readout) {
133
- --_sonic-readout-size: var(--sonic-readout-size, 1.5rem);
134
226
  --_sonic-unit: calc(var(--_sonic-readout-size) / 32);
135
227
 
136
228
  box-sizing: border-box;
@@ -143,19 +235,15 @@
143
235
  position-try-fallbacks: flip-block;
144
236
  overflow: visible;
145
237
  font: inherit;
238
+ font-family: var(--sonic-glass-font);
146
239
  font-size: calc(0.5 * var(--_sonic-readout-size));
147
240
  font-variant-numeric: tabular-nums;
148
241
  line-height: 1;
149
- color: color-mix(in oklab, var(--_sonic-glass) 20%, #fff);
242
+ color: var(--_sonic-glass-text);
150
243
  pointer-events: none;
151
244
  cursor: default;
152
245
  user-select: none;
153
- background: radial-gradient(
154
- closest-side,
155
- color-mix(in oklab, var(--_sonic-glass) 88%, #fff),
156
- var(--_sonic-glass)
157
- )
158
- var(--_sonic-glass);
246
+ background: var(--_sonic-glass-fill);
159
247
  border: 0;
160
248
  border-radius: calc(0.25 * var(--_sonic-readout-size));
161
249
  box-shadow:
@@ -200,7 +288,7 @@
200
288
  cursor: text;
201
289
  }
202
290
 
203
- :is(.sonic-dial-entry, .sonic-slider-entry) {
291
+ :is(.sonic-dial-entry, .sonic-number-entry, .sonic-slider-entry) {
204
292
  inline-size: 6ch;
205
293
  padding: 0;
206
294
  font: inherit;
@@ -226,14 +314,49 @@
226
314
  }
227
315
  }
228
316
 
317
+ /* Printed on the consumer's panel rather than the cap, so the text takes the page's colour and font */
318
+ :is(.sonic-dial-scale, .sonic-slider-scale) {
319
+ position: absolute;
320
+ inset: 0;
321
+ line-height: 1;
322
+ white-space: nowrap;
323
+ pointer-events: none;
324
+
325
+ & > * {
326
+ position: absolute;
327
+ }
328
+
329
+ & svg {
330
+ display: block;
331
+ inline-size: 1em;
332
+ block-size: 1em;
333
+ }
334
+
335
+ & > .sonic-scale-tick {
336
+ background: var(--_sonic-etch);
337
+ }
338
+ }
339
+
229
340
  @media (forced-colors: active) {
230
- :is(.sonic-dial, .sonic-key, .sonic-meter, .sonic-segmented-segment, .sonic-slider) {
341
+ .sonic-scale-tick {
342
+ forced-color-adjust: none;
343
+ }
344
+
345
+ :is(
346
+ .sonic-dial,
347
+ .sonic-key,
348
+ .sonic-led,
349
+ .sonic-meter,
350
+ .sonic-number,
351
+ .sonic-segmented-segment,
352
+ .sonic-slider
353
+ ) {
231
354
  --_sonic-lit: Highlight;
232
355
  --_sonic-unlit: GrayText;
233
356
  }
234
357
 
235
358
  /* Tokens rather than properties, so a state rule that restates one still paints in system colours */
236
- :is(.sonic-dial, .sonic-key, .sonic-segmented-segment, .sonic-slider) {
359
+ :is(.sonic-dial, .sonic-key, .sonic-number, .sonic-segmented-segment, .sonic-slider) {
237
360
  --_sonic-ink: ButtonText;
238
361
  --_sonic-etch: ButtonText;
239
362
  --_sonic-lit-disabled: GrayText;
@@ -253,7 +376,7 @@
253
376
 
254
377
  :is(
255
378
  :disabled > :is(.sonic-dial, .sonic-slider),
256
- .sonic-key:disabled,
379
+ .sonic-key:is(:disabled, [aria-disabled='true']),
257
380
  .sonic-segmented-segment:disabled
258
381
  ) {
259
382
  --_sonic-ink: GrayText;
@@ -261,7 +384,13 @@
261
384
  }
262
385
 
263
386
  /* Stands in for the glow, which forced colours drop; inset, it merged with the cap's rim */
264
- :is(.sonic-dial, .sonic-key, .sonic-segmented-segment, .sonic-slider):focus-visible {
387
+ :is(
388
+ .sonic-dial,
389
+ .sonic-key,
390
+ .sonic-number,
391
+ .sonic-segmented-segment,
392
+ .sonic-slider
393
+ ):focus-visible {
265
394
  outline-offset: 2px;
266
395
  }
267
396
 
@@ -270,7 +399,8 @@
270
399
  forced-color-adjust: none;
271
400
  }
272
401
 
273
- :is(.sonic-dial-readout, .sonic-slider-readout) {
402
+ /* Glass is forced to Canvas like the page, so only a border shows its edge */
403
+ :is(.sonic-dial-readout, .sonic-number, .sonic-screen, .sonic-slider-readout) {
274
404
  border: 1px solid CanvasText;
275
405
  }
276
406
  }
@@ -1,3 +1,21 @@
1
+ @property --_sonic-meter-size {
2
+ syntax: '<length>';
3
+ inherits: true;
4
+ initial-value: 8px;
5
+ }
6
+
7
+ @property --_sonic-meter-length {
8
+ syntax: '<length>';
9
+ inherits: true;
10
+ initial-value: 128px;
11
+ }
12
+
13
+ @property --_sonic-meter-pitch {
14
+ syntax: '<length>';
15
+ inherits: true;
16
+ initial-value: 4px;
17
+ }
18
+
1
19
  /* The size is the thickness */
2
20
  @layer sonic {
3
21
  :is(sonic-meter, .sonic-meter) {
@@ -39,6 +57,7 @@
39
57
  --_sonic-meter-toward: to top;
40
58
  --_sonic-meter-inset: calc(var(--_sonic-meter-size) * 0.125);
41
59
  --_sonic-meter-lens: calc(var(--_sonic-meter-size) - 2 * var(--_sonic-meter-inset));
60
+ --_sonic-meter-clip-space: calc(var(--_sonic-meter-inset) + var(--_sonic-meter-lens));
42
61
  --_sonic-meter-gap: calc(var(--_sonic-meter-pitch) * var(--_sonic-meter-gap-ratio));
43
62
  --_sonic-meter-segment: calc(var(--_sonic-meter-pitch) - var(--_sonic-meter-gap));
44
63
  --_sonic-meter-segment-radius: calc(
@@ -46,7 +65,7 @@
46
65
  );
47
66
  --_sonic-meter-segments: round(
48
67
  down,
49
- var(--_sonic-meter-length) - 3 * var(--_sonic-meter-inset) - var(--_sonic-meter-lens) +
68
+ var(--_sonic-meter-length) - 2 * var(--_sonic-meter-inset) - var(--_sonic-meter-clip-space) +
50
69
  var(--_sonic-meter-gap),
51
70
  var(--_sonic-meter-pitch)
52
71
  );
@@ -75,6 +94,26 @@
75
94
  block-size: var(--_sonic-meter-size);
76
95
  }
77
96
 
97
+ /* No clip on either, so the lights take the lens's room */
98
+ :is([scale='linear'], :state(ladder)) > .sonic-meter {
99
+ --_sonic-meter-clip-space: 0px;
100
+ }
101
+
102
+ :state(ladder) > .sonic-meter {
103
+ --_sonic-meter-pitch: calc(
104
+ (var(--_sonic-meter-length) - 2 * var(--_sonic-meter-inset)) / var(--_sonic-meter-count, 1)
105
+ );
106
+ --_sonic-meter-gap: 0px;
107
+ --_sonic-meter-segments: calc(var(--_sonic-meter-length) - 2 * var(--_sonic-meter-inset));
108
+ --_sonic-meter-light-radius: calc(
109
+ min(
110
+ var(--_sonic-meter-lens),
111
+ var(--_sonic-meter-pitch) * (1 - var(--_sonic-meter-gap-ratio))
112
+ ) /
113
+ 2
114
+ );
115
+ }
116
+
78
117
  /* Snapped to whole lights, so none is split between two colours */
79
118
  :is(.sonic-meter-segments, .sonic-meter-level) {
80
119
  --_sonic-meter-span: calc(var(--_sonic-meter-max, 0) - var(--_sonic-meter-min, -60));
@@ -90,7 +129,9 @@
90
129
  --_sonic-meter-clip-at: round(
91
130
  clamp(
92
131
  0,
93
- (var(--_sonic-meter-clip-from) - var(--_sonic-meter-min, -60)) / var(--_sonic-meter-span),
132
+ /* fallow-ignore-next-line code-duplication -- each zone snaps its own threshold by the same rule */
133
+ (var(--_sonic-meter-clip-from) - var(--_sonic-meter-min, -60)) /
134
+ var(--_sonic-meter-span),
94
135
  1
95
136
  ) *
96
137
  var(--_sonic-meter-segments),
@@ -126,6 +167,7 @@
126
167
  0 100% / 100% var(--_sonic-meter-pitch) no-repeat repeat,
127
168
  radial-gradient(
128
169
  circle var(--_sonic-meter-segment-radius) at left var(--_sonic-meter-segment-radius)
170
+ /* fallow-ignore-next-line code-duplication -- one gradient per segment corner is geometry, not copied logic */
129
171
  bottom calc(var(--_sonic-meter-segment) - var(--_sonic-meter-segment-radius)),
130
172
  #000 calc(100% - 0.5px),
131
173
  transparent
@@ -146,6 +188,23 @@
146
188
  mask-composite: add;
147
189
  }
148
190
 
191
+ :state(ladder) > .sonic-meter > :is(.sonic-meter-segments, .sonic-meter-level) {
192
+ --_sonic-meter-hot-at: calc(var(--_sonic-meter-hot-lights, 0) * var(--_sonic-meter-pitch));
193
+ --_sonic-meter-clip-at: calc(var(--_sonic-meter-clip-lights, 0) * var(--_sonic-meter-pitch));
194
+ --_sonic-meter-lights: radial-gradient(
195
+ circle var(--_sonic-meter-light-radius),
196
+ #000 calc(100% - 0.5px),
197
+ transparent
198
+ )
199
+ 0 100% / 100% var(--_sonic-meter-pitch) no-repeat repeat;
200
+ }
201
+
202
+ /* The zones are dBFS, which a linear level is not */
203
+ [scale='linear'] > .sonic-meter > :is(.sonic-meter-segments, .sonic-meter-level) {
204
+ --_sonic-meter-hot-at: var(--_sonic-meter-segments);
205
+ --_sonic-meter-clip-at: var(--_sonic-meter-segments);
206
+ }
207
+
149
208
  [orientation='horizontal'] > .sonic-meter > :is(.sonic-meter-segments, .sonic-meter-level) {
150
209
  --_sonic-meter-lights:
151
210
  linear-gradient(to right, #000 var(--_sonic-meter-segment), transparent 0) 0
@@ -176,6 +235,7 @@
176
235
  radial-gradient(
177
236
  circle var(--_sonic-meter-segment-radius) at left
178
237
  calc(var(--_sonic-meter-segment) - var(--_sonic-meter-segment-radius)) top
238
+ /* fallow-ignore-next-line code-duplication -- one gradient per segment corner is geometry, not copied logic */
179
239
  var(--_sonic-meter-segment-radius),
180
240
  #000 calc(100% - 0.5px),
181
241
  transparent
@@ -195,6 +255,42 @@
195
255
  block-size: auto;
196
256
  }
197
257
 
258
+ [orientation='horizontal']:state(ladder)
259
+ > .sonic-meter
260
+ > :is(.sonic-meter-segments, .sonic-meter-level) {
261
+ --_sonic-meter-lights: radial-gradient(
262
+ circle var(--_sonic-meter-light-radius),
263
+ #000 calc(100% - 0.5px),
264
+ transparent
265
+ )
266
+ 0 0 / var(--_sonic-meter-pitch) 100% repeat no-repeat;
267
+ }
268
+
269
+ :state(ladder) > .sonic-meter > :is(.sonic-meter-segments, .sonic-meter-level)::after {
270
+ position: absolute;
271
+ inset: 0;
272
+ content: '';
273
+ background: linear-gradient(var(--_sonic-light-fall), rgb(255 255 255 / 10%), transparent 60%) 0
274
+ 100% / 100% var(--_sonic-meter-pitch) repeat-y;
275
+ }
276
+
277
+ :state(ladder) > .sonic-meter > .sonic-meter-level::after {
278
+ background: radial-gradient(
279
+ circle var(--_sonic-meter-light-radius),
280
+ rgb(255 255 255 / 45%),
281
+ transparent
282
+ )
283
+ 0 100% / 100% var(--_sonic-meter-pitch) repeat-y;
284
+ }
285
+
286
+ [orientation='horizontal']:state(ladder)
287
+ > .sonic-meter
288
+ > :is(.sonic-meter-segments, .sonic-meter-level)::after {
289
+ background-repeat: repeat-x;
290
+ background-position: 0 0;
291
+ background-size: var(--_sonic-meter-pitch) 100%;
292
+ }
293
+
198
294
  .sonic-meter-segments {
199
295
  background: linear-gradient(
200
296
  var(--_sonic-meter-toward),
@@ -205,15 +301,24 @@
205
301
  }
206
302
 
207
303
  /* One mask for the bar and peak, so a frame repaints without layout */
304
+
305
+ /* The hair of slack keeps an exact fraction from losing a light to float error */
208
306
  .sonic-meter-level {
307
+ --_sonic-meter-lit-from: round(
308
+ up,
309
+ min(var(--_sonic-meter-origin, 0), var(--_sonic-meter-level, 0)) *
310
+ var(--_sonic-meter-segments) - 0.01px,
311
+ var(--_sonic-meter-pitch)
312
+ );
209
313
  --_sonic-meter-lit: round(
210
314
  down,
211
- var(--_sonic-meter-level, 0) * var(--_sonic-meter-segments),
315
+ max(var(--_sonic-meter-origin, 0), var(--_sonic-meter-level, 0)) *
316
+ var(--_sonic-meter-segments) + 0.01px,
212
317
  var(--_sonic-meter-pitch)
213
318
  );
214
319
  --_sonic-meter-held: round(
215
320
  down,
216
- var(--_sonic-meter-peak, 0) * var(--_sonic-meter-segments),
321
+ var(--_sonic-meter-peak, 0) * var(--_sonic-meter-segments) + 0.01px,
217
322
  var(--_sonic-meter-pitch)
218
323
  );
219
324
 
@@ -226,7 +331,8 @@
226
331
  mask:
227
332
  linear-gradient(
228
333
  var(--_sonic-meter-toward),
229
- #000 var(--_sonic-meter-lit),
334
+ transparent var(--_sonic-meter-lit-from),
335
+ #000 0 var(--_sonic-meter-lit),
230
336
  transparent 0 calc(var(--_sonic-meter-held) - var(--_sonic-meter-pitch)),
231
337
  #000 0 var(--_sonic-meter-held),
232
338
  transparent 0
@@ -239,9 +345,7 @@
239
345
  position: absolute;
240
346
  inset: var(--_sonic-meter-inset) var(--_sonic-meter-inset) auto;
241
347
  block-size: var(--_sonic-meter-lens);
242
- background:
243
- linear-gradient(var(--_sonic-light-fall), rgb(255 255 255 / 10%), transparent 60%),
244
- color-mix(in oklab, var(--_sonic-unlit) 60%, #000);
348
+ background: var(--_sonic-lens);
245
349
  border-radius: 50%;
246
350
 
247
351
  &::after {
@@ -258,6 +362,10 @@
258
362
  }
259
363
  }
260
364
 
365
+ :is([scale='linear'], :state(ladder)) > .sonic-meter > .sonic-meter-clip {
366
+ display: none;
367
+ }
368
+
261
369
  [orientation='horizontal'] > .sonic-meter > .sonic-meter-clip {
262
370
  inset: var(--_sonic-meter-inset) var(--_sonic-meter-inset) var(--_sonic-meter-inset) auto;
263
371
  inline-size: var(--_sonic-meter-lens);
@@ -287,5 +395,9 @@
287
395
  .sonic-meter-clip::after {
288
396
  background: Mark;
289
397
  }
398
+
399
+ :state(ladder) > .sonic-meter > :is(.sonic-meter-segments, .sonic-meter-level)::after {
400
+ display: none;
401
+ }
290
402
  }
291
403
  }
@@ -0,0 +1,81 @@
1
+ @property --_sonic-number-size {
2
+ syntax: '<length>';
3
+ inherits: true;
4
+ initial-value: 28px;
5
+ }
6
+
7
+ @property --_sonic-number-travel {
8
+ syntax: '<length>';
9
+ inherits: false;
10
+ initial-value: 160px;
11
+ }
12
+
13
+ @layer sonic {
14
+ :is(sonic-number, .sonic-number) {
15
+ --_sonic-number-size: var(--sonic-number-size, 1.75rem);
16
+ --_sonic-number-aspect-ratio: var(--sonic-number-aspect-ratio, 3);
17
+ }
18
+
19
+ :has(> .sonic-number):not([hidden]) {
20
+ display: contents;
21
+ }
22
+
23
+ sonic-number:not(:defined, [hidden]) {
24
+ display: block;
25
+ flex-shrink: 0;
26
+ inline-size: calc(var(--_sonic-number-size) * var(--_sonic-number-aspect-ratio));
27
+ block-size: var(--_sonic-number-size);
28
+ }
29
+
30
+ .sonic-number {
31
+ --_sonic-number-radius-ratio: var(--sonic-number-radius-ratio, 0.15);
32
+ --_sonic-number-inset-ratio: var(--sonic-number-inset-ratio, 0.2);
33
+ --_sonic-number-text-ratio: var(--sonic-number-text-ratio, 0.45);
34
+ --_sonic-number-travel: var(--sonic-number-travel, 160px);
35
+ --_sonic-unit: calc(var(--_sonic-number-size) / 32);
36
+
37
+ inline-size: calc(var(--_sonic-number-size) * var(--_sonic-number-aspect-ratio));
38
+ block-size: var(--_sonic-number-size);
39
+ padding: 0 calc(var(--_sonic-number-size) * var(--_sonic-number-inset-ratio));
40
+ font-size: calc(var(--_sonic-number-size) * var(--_sonic-number-text-ratio));
41
+ font-variant-numeric: tabular-nums;
42
+ line-height: 1;
43
+ touch-action: none;
44
+ cursor: ns-resize;
45
+ border-radius: calc(var(--_sonic-number-size) * var(--_sonic-number-radius-ratio));
46
+ box-shadow: var(--_sonic-focus-shadow);
47
+ }
48
+
49
+ :disabled > .sonic-number {
50
+ color: var(--_sonic-etch);
51
+ cursor: default;
52
+ }
53
+
54
+ :is(.sonic-number-value, .sonic-number-entry) {
55
+ grid-area: 1 / 1;
56
+ max-inline-size: 100%;
57
+ overflow: hidden;
58
+ white-space: nowrap;
59
+ }
60
+
61
+ :state(editing) > .sonic-number > .sonic-number-value {
62
+ visibility: hidden;
63
+ }
64
+
65
+ .sonic-number-entry {
66
+ inline-size: 100%;
67
+ cursor: text;
68
+ field-sizing: fixed;
69
+ }
70
+
71
+ @media (forced-colors: active) {
72
+ .sonic-number-value {
73
+ color: CanvasText;
74
+ forced-color-adjust: none;
75
+ }
76
+
77
+ :disabled > .sonic-number > .sonic-number-value {
78
+ color: GrayText;
79
+ }
80
+ }
81
+ }