@xsynaptic/sonic-ui 0.1.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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/dist/define/dial.d.ts +2 -0
  4. package/dist/define/dial.js +8 -0
  5. package/dist/define/dial.js.map +1 -0
  6. package/dist/define/key.d.ts +2 -0
  7. package/dist/define/key.js +8 -0
  8. package/dist/define/key.js.map +1 -0
  9. package/dist/define/meter.d.ts +2 -0
  10. package/dist/define/meter.js +8 -0
  11. package/dist/define/meter.js.map +1 -0
  12. package/dist/define/segmented.d.ts +2 -0
  13. package/dist/define/segmented.js +8 -0
  14. package/dist/define/segmented.js.map +1 -0
  15. package/dist/define/slider.d.ts +2 -0
  16. package/dist/define/slider.js +8 -0
  17. package/dist/define/slider.js.map +1 -0
  18. package/dist/define.d.ts +11 -0
  19. package/dist/define.js +5 -0
  20. package/dist/elements/define-once.js +8 -0
  21. package/dist/elements/define-once.js.map +1 -0
  22. package/dist/elements/dial.d.ts +15 -0
  23. package/dist/elements/dial.js +57 -0
  24. package/dist/elements/dial.js.map +1 -0
  25. package/dist/elements/key.d.ts +17 -0
  26. package/dist/elements/key.js +99 -0
  27. package/dist/elements/key.js.map +1 -0
  28. package/dist/elements/meter.d.ts +17 -0
  29. package/dist/elements/meter.js +119 -0
  30. package/dist/elements/meter.js.map +1 -0
  31. package/dist/elements/range-element.d.ts +32 -0
  32. package/dist/elements/range-element.js +348 -0
  33. package/dist/elements/range-element.js.map +1 -0
  34. package/dist/elements/range-entry.js +73 -0
  35. package/dist/elements/range-entry.js.map +1 -0
  36. package/dist/elements/segmented.d.ts +17 -0
  37. package/dist/elements/segmented.js +104 -0
  38. package/dist/elements/segmented.js.map +1 -0
  39. package/dist/elements/slider.d.ts +15 -0
  40. package/dist/elements/slider.js +70 -0
  41. package/dist/elements/slider.js.map +1 -0
  42. package/dist/elements/sonic-element.d.ts +20 -0
  43. package/dist/elements/sonic-element.js +94 -0
  44. package/dist/elements/sonic-element.js.map +1 -0
  45. package/dist/index.d.ts +6 -0
  46. package/dist/index.js +7 -0
  47. package/dist/lib/read-px-property.js +9 -0
  48. package/dist/lib/read-px-property.js.map +1 -0
  49. package/dist/lib/render.js +19 -0
  50. package/dist/lib/render.js.map +1 -0
  51. package/dist/lib/taper.js +33 -0
  52. package/dist/lib/taper.js.map +1 -0
  53. package/dist/lib/write-attribute.js +12 -0
  54. package/dist/lib/write-attribute.js.map +1 -0
  55. package/dist/skins/ivory.css +8 -0
  56. package/dist/skins/lime.css +10 -0
  57. package/dist/skins/slate.css +10 -0
  58. package/dist/styles/controls.css +8 -0
  59. package/dist/styles/dial.css +265 -0
  60. package/dist/styles/key.css +137 -0
  61. package/dist/styles/material.css +229 -0
  62. package/dist/styles/meter.css +271 -0
  63. package/dist/styles/screen.css +47 -0
  64. package/dist/styles/segmented.css +147 -0
  65. package/dist/styles/slider.css +204 -0
  66. package/package.json +76 -0
@@ -0,0 +1,137 @@
1
+ @layer sonic {
2
+ /* On the host too, for the placeholder */
3
+ :is(sonic-key, .sonic-key) {
4
+ --_sonic-key-size: var(--sonic-key-size, 2rem);
5
+ }
6
+
7
+ :has(> .sonic-key):not([hidden]) {
8
+ display: contents;
9
+ }
10
+
11
+ /* Hides the icon, which has no size before upgrade */
12
+ sonic-key:not(:defined, [hidden]) {
13
+ display: block;
14
+ visibility: hidden;
15
+ flex-shrink: 0;
16
+ inline-size: var(--_sonic-key-size);
17
+ block-size: var(--_sonic-key-size);
18
+ overflow: hidden;
19
+ }
20
+
21
+ /* The button is the well; the cap is the part that moves */
22
+ .sonic-key {
23
+ --_sonic-key-gap-ratio: var(--sonic-key-gap-ratio, 0);
24
+ --_sonic-key-radius-ratio: var(--sonic-key-radius-ratio, 0.2);
25
+ --_sonic-key-bevel-ratio: var(--sonic-key-bevel-ratio, 0.075);
26
+ --_sonic-key-face-radius-ratio: var(--sonic-key-face-radius-ratio, 0.15);
27
+ --_sonic-key-icon-ratio: var(--sonic-key-icon-ratio, 0.5);
28
+ --_sonic-key-depth-ratio: var(--sonic-key-depth-ratio, 0.03);
29
+ --_sonic-key-press-scale: var(--sonic-key-press-scale, 0.985);
30
+ --_sonic-unit: calc(var(--_sonic-key-size) / 32);
31
+ --_sonic-key-corner: calc(var(--_sonic-key-size) * var(--_sonic-key-radius-ratio));
32
+ --_sonic-key-gap: calc(var(--_sonic-key-size) * var(--_sonic-key-gap-ratio));
33
+ --_sonic-key-depth: calc(var(--_sonic-key-size) * var(--_sonic-key-depth-ratio));
34
+ --_sonic-key-shift: calc(var(--_sonic-key-depth) / -2);
35
+ --_sonic-key-scale: 1;
36
+ --_sonic-key-drop: calc(1.5 * var(--_sonic-cast-x)) calc(1.5 * var(--_sonic-cast-y))
37
+ calc(2 * var(--_sonic-unit)) rgb(0 0 0 / 65%);
38
+ --_sonic-key-glow: transparent;
39
+
40
+ position: relative;
41
+ flex-shrink: 0;
42
+ inline-size: var(--_sonic-key-size);
43
+ block-size: var(--_sonic-key-size);
44
+ padding: 0;
45
+ color: var(--_sonic-ink);
46
+ cursor: pointer;
47
+ background: linear-gradient(
48
+ var(--_sonic-light-fall),
49
+ color-mix(in oklab, var(--_sonic-unlit) 55%, #000),
50
+ color-mix(in oklab, var(--_sonic-unlit) 85%, #000)
51
+ );
52
+ border: 0;
53
+ border-radius: var(--_sonic-key-corner);
54
+ box-shadow: var(--_sonic-well);
55
+ }
56
+
57
+ /* A drifting finger would turn a held pad into a pan */
58
+ :where([momentary]) > .sonic-key {
59
+ touch-action: none;
60
+ }
61
+
62
+ .sonic-key:active:where(:not(:disabled)),
63
+ .sonic-key:is([aria-pressed='true'], :where([momentary][pressed]) > *) {
64
+ --_sonic-key-shift: calc(var(--_sonic-key-depth) / 2);
65
+ --_sonic-key-scale: var(--_sonic-key-press-scale);
66
+ --_sonic-key-drop: calc(0.5 * var(--_sonic-cast-x)) calc(0.5 * var(--_sonic-cast-y))
67
+ var(--_sonic-unit) rgb(0 0 0 / 65%);
68
+ }
69
+
70
+ .sonic-key:is([aria-pressed='true'], :where([momentary][pressed]) > *) {
71
+ --_sonic-key-glow: color-mix(in oklab, var(--_sonic-lit) 22%, transparent);
72
+
73
+ color: var(--_sonic-lit);
74
+
75
+ /* A full-strength floor shows as a hard line where the sunk cap uncovers it */
76
+ background: linear-gradient(
77
+ var(--_sonic-light-fall),
78
+ color-mix(in oklab, var(--_sonic-lit) 25%, #000),
79
+ color-mix(in oklab, var(--_sonic-lit) 60%, #000)
80
+ );
81
+ }
82
+
83
+ .sonic-key:disabled {
84
+ --_sonic-lit: var(--_sonic-lit-disabled);
85
+
86
+ color: var(--_sonic-etch);
87
+ cursor: default;
88
+ }
89
+
90
+ .sonic-key-cap {
91
+ position: absolute;
92
+ inset: var(--_sonic-key-gap);
93
+ display: grid;
94
+ place-items: center;
95
+ background: var(--_sonic-skirt);
96
+ border-radius: max(0px, var(--_sonic-key-corner) - var(--_sonic-key-gap));
97
+ box-shadow: var(--_sonic-key-drop), var(--_sonic-rim), var(--_sonic-focus-shadow);
98
+ scale: var(--_sonic-key-scale);
99
+ translate: 0 var(--_sonic-key-shift);
100
+ transition: var(--_sonic-press-transition);
101
+
102
+ &::before {
103
+ position: absolute;
104
+ inset: calc(var(--_sonic-key-size) * var(--_sonic-key-bevel-ratio));
105
+ content: '';
106
+ background:
107
+ radial-gradient(closest-side, var(--_sonic-key-glow), transparent), var(--_sonic-sheen),
108
+ var(--_sonic-face);
109
+ border-radius: calc(var(--_sonic-key-size) * var(--_sonic-key-face-radius-ratio));
110
+ box-shadow: var(--_sonic-face-shadow);
111
+ }
112
+
113
+ & > svg {
114
+ position: relative;
115
+ inline-size: calc(var(--_sonic-key-size) * var(--_sonic-key-icon-ratio));
116
+ block-size: calc(var(--_sonic-key-size) * var(--_sonic-key-icon-ratio));
117
+ filter: var(--_sonic-engraving);
118
+ mask-image: linear-gradient(var(--_sonic-light-fall), #000 15%, rgb(0 0 0 / 60%));
119
+ mask-repeat: no-repeat;
120
+ }
121
+ }
122
+
123
+ /* Engraving under the glow keeps a lit icon's edge */
124
+ .sonic-key:is([aria-pressed='true'], :where([momentary][pressed]) > *) .sonic-key-cap > svg {
125
+ filter: var(--_sonic-engraving)
126
+ drop-shadow(0 0 var(--_sonic-unit) color-mix(in oklab, var(--_sonic-lit) 35%, transparent));
127
+ }
128
+
129
+ .sonic-key:is([aria-pressed='true'], :where([momentary][pressed]) > *)
130
+ .sonic-key-cap
131
+ > [data-sonic-when='off'],
132
+ .sonic-key:not([aria-pressed='true'], :where([momentary][pressed]) > *)
133
+ .sonic-key-cap
134
+ > [data-sonic-when='on'] {
135
+ display: none;
136
+ }
137
+ }
@@ -0,0 +1,229 @@
1
+ @layer sonic {
2
+ /* Resolved per control rather than on `:root`, so each follows its own cap and unit */
3
+ :is(
4
+ .sonic-dial,
5
+ .sonic-dial-readout,
6
+ .sonic-key,
7
+ .sonic-meter,
8
+ .sonic-screen,
9
+ .sonic-segmented,
10
+ .sonic-slider,
11
+ .sonic-slider-readout
12
+ ) {
13
+ --_sonic-unlit: var(--sonic-unlit, #27272a);
14
+
15
+ /* Degrees left of overhead */
16
+ --_sonic-light-tilt: var(--sonic-light-tilt, 17deg);
17
+ --_sonic-light-fall: calc(180deg - var(--_sonic-light-tilt));
18
+ --_sonic-cast-x: calc(sin(var(--_sonic-light-tilt)) * var(--_sonic-unit));
19
+ --_sonic-cast-y: calc(cos(var(--_sonic-light-tilt)) * var(--_sonic-unit));
20
+ }
21
+
22
+ :is(.sonic-dial, .sonic-key, .sonic-meter, .sonic-segmented-segment, .sonic-slider) {
23
+ --_sonic-lit: var(--sonic-lit, #f59e0b);
24
+ }
25
+
26
+ .sonic-meter {
27
+ --_sonic-hot: var(--sonic-hot, #f97316);
28
+ --_sonic-clip: var(--sonic-clip, #ef4444);
29
+ }
30
+
31
+ :is(.sonic-dial, .sonic-key, .sonic-segmented-segment, .sonic-slider) {
32
+ --_sonic-cap: var(--sonic-cap, #3f3f46);
33
+ --_sonic-cap-hover: var(--sonic-cap-hover, #52525b);
34
+ --_sonic-focus: var(--sonic-focus, oklch(90% 0.23 128deg));
35
+
36
+ /* Derived from the cap, so a reskinned cap brings its marks along */
37
+ --_sonic-ink: var(--sonic-mark, color-mix(in oklab, var(--_sonic-cap) 30%, #fff));
38
+ --_sonic-etch: color-mix(in oklab, var(--_sonic-ink) 30%, transparent);
39
+ --_sonic-lit-disabled: color-mix(in oklab, var(--_sonic-ink) 25%, var(--_sonic-unlit));
40
+ --_sonic-skirt: linear-gradient(
41
+ var(--_sonic-light-fall),
42
+ color-mix(in oklab, var(--_sonic-cap) 70%, #fff),
43
+ color-mix(in oklab, var(--_sonic-cap) 55%, #000)
44
+ );
45
+
46
+ /* Inverted from the skirt, as a dished face takes light */
47
+ --_sonic-face: linear-gradient(
48
+ var(--_sonic-light-fall),
49
+ color-mix(in oklab, var(--_sonic-cap) 85%, #000),
50
+ color-mix(in oklab, var(--_sonic-cap) 90%, #fff)
51
+ );
52
+ --_sonic-sheen: linear-gradient(
53
+ 105deg,
54
+ transparent 20%,
55
+ rgb(255 255 255 / 10%) 45%,
56
+ transparent 55%,
57
+ rgb(0 0 0 / 6%) 80%
58
+ );
59
+ --_sonic-press-transition:
60
+ scale 60ms cubic-bezier(0.4, 0, 0.2, 1), translate 60ms cubic-bezier(0.4, 0, 0.2, 1),
61
+ box-shadow 60ms cubic-bezier(0.4, 0, 0.2, 1);
62
+ --_sonic-face-shadow:
63
+ inset var(--_sonic-cast-x) var(--_sonic-cast-y) calc(1.5 * var(--_sonic-unit))
64
+ rgb(0 0 0 / 35%),
65
+ calc(0.5 * var(--_sonic-cast-x)) calc(0.5 * var(--_sonic-cast-y)) var(--_sonic-unit)
66
+ rgb(255 255 255 / 12%);
67
+ --_sonic-rim: 0 0 0 max(1px, 0.75 * var(--_sonic-unit))
68
+ color-mix(in oklab, var(--_sonic-cap) 40%, #000);
69
+ --_sonic-focus-shadow: 0 0 transparent;
70
+ --_sonic-focus-glow: 0 0 max(4px, 3 * var(--_sonic-unit)) max(1px, 0.5 * var(--_sonic-unit))
71
+ color-mix(in oklab, var(--_sonic-focus) 60%, transparent);
72
+ }
73
+
74
+ :is(.sonic-dial, .sonic-slider) {
75
+ --_sonic-lit-off: var(
76
+ --sonic-dimmed,
77
+ color-mix(in oklab, var(--_sonic-lit) 50%, var(--_sonic-unlit))
78
+ );
79
+ }
80
+
81
+ :is(.sonic-dial-readout, .sonic-screen, .sonic-slider-readout) {
82
+ --_sonic-glass: var(--sonic-glass, color-mix(in oklab, var(--_sonic-unlit) 45%, #000));
83
+ }
84
+
85
+ :is(.sonic-key, .sonic-screen, .sonic-segmented) {
86
+ --_sonic-well:
87
+ inset var(--_sonic-cast-x) var(--_sonic-cast-y) calc(2 * var(--_sonic-unit)) rgb(0 0 0 / 60%),
88
+ calc(0.5 * var(--_sonic-cast-x)) calc(0.5 * var(--_sonic-cast-y)) 0 rgb(255 255 255 / 6%);
89
+ }
90
+
91
+ :is(.sonic-key, .sonic-segmented) {
92
+ --_sonic-engraving: drop-shadow(
93
+ calc(-0.4 * var(--_sonic-cast-x)) calc(-0.4 * var(--_sonic-cast-y))
94
+ calc(0.4 * var(--_sonic-unit)) rgb(0 0 0 / 30%)
95
+ )
96
+ drop-shadow(
97
+ calc(0.3 * var(--_sonic-cast-x)) calc(0.3 * var(--_sonic-cast-y)) 0 rgb(255 255 255 / 7%)
98
+ );
99
+ }
100
+
101
+ /* The well's full blur would fill a slot this narrow */
102
+ :is(.sonic-meter, .sonic-slider) {
103
+ --_sonic-groove:
104
+ inset var(--_sonic-cast-x) var(--_sonic-cast-y) calc(0.75 * var(--_sonic-unit))
105
+ rgb(0 0 0 / 55%),
106
+ inset 0 0 calc(0.5 * var(--_sonic-unit)) rgb(0 0 0 / 30%),
107
+ calc(0.5 * var(--_sonic-cast-x)) calc(0.5 * var(--_sonic-cast-y)) 0 rgb(255 255 255 / 6%);
108
+ }
109
+
110
+ @media (hover: hover) {
111
+ :is(.sonic-dial, .sonic-key, .sonic-segmented-segment, .sonic-slider):not(
112
+ :disabled,
113
+ [aria-disabled='true']
114
+ ):hover {
115
+ --_sonic-cap: var(--_sonic-cap-hover);
116
+ }
117
+ }
118
+
119
+ :is(.sonic-dial, .sonic-key, .sonic-segmented-segment, .sonic-slider):focus-visible {
120
+ --_sonic-cap: var(--_sonic-cap-hover);
121
+ --_sonic-focus-shadow: var(--_sonic-focus-glow);
122
+
123
+ /* Painted only in forced colours; inset so it adds no size */
124
+ outline: 2px solid transparent;
125
+ outline-offset: -2px;
126
+ }
127
+
128
+ /* Keeps the glow while focus is in the entry; its own rule, since an engine without `:state()` drops a whole selector list */
129
+ :state(editing) > :is(.sonic-dial, .sonic-slider) {
130
+ --_sonic-cap: var(--_sonic-cap-hover);
131
+ --_sonic-focus-shadow: var(--_sonic-focus-glow);
132
+ }
133
+
134
+ :is(.sonic-dial-readout, .sonic-slider-readout) {
135
+ --_sonic-readout-size: var(--sonic-readout-size, 1.5rem);
136
+ --_sonic-unit: calc(var(--_sonic-readout-size) / 32);
137
+
138
+ box-sizing: border-box;
139
+ display: grid;
140
+ place-items: center;
141
+ min-inline-size: calc(1.75 * var(--_sonic-readout-size));
142
+ block-size: var(--_sonic-readout-size);
143
+ padding: 0 calc(0.3 * var(--_sonic-readout-size));
144
+ margin: 0 0 calc(0.25 * var(--_sonic-readout-size));
145
+ position-area: block-start;
146
+ position-try-fallbacks: flip-block;
147
+ overflow: visible;
148
+ font: inherit;
149
+ font-size: calc(0.5 * var(--_sonic-readout-size));
150
+ font-variant-numeric: tabular-nums;
151
+ line-height: 1;
152
+ color: color-mix(in oklab, var(--_sonic-glass) 20%, #fff);
153
+ pointer-events: none;
154
+ cursor: default;
155
+ user-select: none;
156
+ background: radial-gradient(
157
+ closest-side,
158
+ color-mix(in oklab, var(--_sonic-glass) 88%, #fff),
159
+ var(--_sonic-glass)
160
+ )
161
+ var(--_sonic-glass);
162
+ border: 0;
163
+ border-radius: calc(0.25 * var(--_sonic-readout-size));
164
+ box-shadow:
165
+ calc(2 * var(--_sonic-cast-x)) calc(2 * var(--_sonic-cast-y)) calc(6 * var(--_sonic-unit))
166
+ rgb(0 0 0 / 55%),
167
+ inset 0 0 0 max(1px, var(--_sonic-unit)) rgb(255 255 255 / 8%);
168
+ opacity: 0;
169
+ translate: 0 calc(0.15 * var(--_sonic-readout-size));
170
+ transition:
171
+ opacity 120ms ease-out,
172
+ translate 120ms ease-out,
173
+ display 120ms allow-discrete,
174
+ overlay 120ms allow-discrete;
175
+
176
+ &:popover-open {
177
+ opacity: 1;
178
+ translate: 0;
179
+
180
+ @starting-style {
181
+ opacity: 0;
182
+ translate: 0 calc(0.15 * var(--_sonic-readout-size));
183
+ }
184
+ }
185
+
186
+ @media (prefers-reduced-motion: reduce) {
187
+ translate: 0;
188
+
189
+ &:popover-open {
190
+ @starting-style {
191
+ translate: 0;
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ :state(editing)
198
+ > :is(.sonic-dial, .sonic-slider)
199
+ > :is(.sonic-dial-readout, .sonic-slider-readout) {
200
+ pointer-events: auto;
201
+ cursor: text;
202
+ }
203
+
204
+ :is(.sonic-dial-entry, .sonic-slider-entry) {
205
+ inline-size: 6ch;
206
+ padding: 0;
207
+ font: inherit;
208
+ color: inherit;
209
+ text-align: center;
210
+ background: transparent;
211
+ border: 0;
212
+
213
+ &::selection {
214
+ color: var(--_sonic-lit);
215
+ background: transparent;
216
+ }
217
+
218
+ &:focus-visible {
219
+ outline: 2px solid transparent;
220
+ outline-offset: -2px;
221
+ }
222
+
223
+ @supports (field-sizing: content) {
224
+ inline-size: auto;
225
+ min-inline-size: 3ch;
226
+ field-sizing: content;
227
+ }
228
+ }
229
+ }
@@ -0,0 +1,271 @@
1
+ /* The size is the thickness */
2
+ @layer sonic {
3
+ /* On the host too, for the placeholder */
4
+ :is(sonic-meter, .sonic-meter) {
5
+ --_sonic-meter-size: var(--sonic-meter-size, 0.5rem);
6
+ --_sonic-meter-length: var(--sonic-meter-length, 8rem);
7
+ }
8
+
9
+ :has(> .sonic-meter):not([hidden]) {
10
+ display: contents;
11
+ }
12
+
13
+ sonic-meter:not(:defined, [hidden]) {
14
+ display: block;
15
+ flex-shrink: 0;
16
+ inline-size: var(--_sonic-meter-size);
17
+ block-size: var(--_sonic-meter-length);
18
+ }
19
+
20
+ sonic-meter[orientation='horizontal']:not(:defined, [hidden]) {
21
+ inline-size: var(--_sonic-meter-length);
22
+ block-size: var(--_sonic-meter-size);
23
+ }
24
+
25
+ /* Laid on its side rather than rotated, as the slider is */
26
+ .sonic-meter {
27
+ /* Absolute, so a longer meter gains segments rather than stretching them */
28
+ --_sonic-meter-pitch: var(--sonic-meter-segment-pitch, 4px);
29
+
30
+ /* A fraction of the pitch */
31
+ --_sonic-meter-gap-ratio: var(--sonic-meter-gap-ratio, 0.25);
32
+ --_sonic-meter-radius-ratio: var(--sonic-meter-radius-ratio, 0.25);
33
+
34
+ /* A fraction of the light's length along the meter */
35
+ --_sonic-meter-segment-radius-ratio: var(--sonic-meter-segment-radius-ratio, 0.35);
36
+
37
+ /* dBFS */
38
+ --_sonic-meter-hot-from: var(--sonic-meter-hot-from, -12);
39
+ --_sonic-meter-clip-from: var(--sonic-meter-clip-from, -3);
40
+ --_sonic-unit: calc(var(--_sonic-meter-size) / 8);
41
+ --_sonic-meter-toward: to top;
42
+ --_sonic-meter-inset: calc(var(--_sonic-meter-size) * 0.125);
43
+ --_sonic-meter-lens: calc(var(--_sonic-meter-size) - 2 * var(--_sonic-meter-inset));
44
+ --_sonic-meter-gap: calc(var(--_sonic-meter-pitch) * var(--_sonic-meter-gap-ratio));
45
+ --_sonic-meter-segment: calc(var(--_sonic-meter-pitch) - var(--_sonic-meter-gap));
46
+ --_sonic-meter-segment-radius: calc(
47
+ var(--_sonic-meter-segment) * var(--_sonic-meter-segment-radius-ratio)
48
+ );
49
+
50
+ /* Whole segments only, so the last one ends flush */
51
+ --_sonic-meter-segments: round(
52
+ down,
53
+ var(--_sonic-meter-length) - 3 * var(--_sonic-meter-inset) - var(--_sonic-meter-lens) +
54
+ var(--_sonic-meter-gap),
55
+ var(--_sonic-meter-pitch)
56
+ );
57
+
58
+ position: relative;
59
+ flex-shrink: 0;
60
+ inline-size: var(--_sonic-meter-size);
61
+ block-size: var(--_sonic-meter-length);
62
+ background: color-mix(in oklab, var(--_sonic-unlit) 55%, #000);
63
+ border-radius: calc(var(--_sonic-meter-size) * var(--_sonic-meter-radius-ratio));
64
+
65
+ /* Over the segments, so the lip shades them too */
66
+ &::after {
67
+ position: absolute;
68
+ inset: 0;
69
+ pointer-events: none;
70
+ content: '';
71
+ border-radius: inherit;
72
+ box-shadow: var(--_sonic-groove);
73
+ }
74
+ }
75
+
76
+ [orientation='horizontal'] > .sonic-meter {
77
+ --_sonic-meter-toward: to right;
78
+
79
+ inline-size: var(--_sonic-meter-length);
80
+ block-size: var(--_sonic-meter-size);
81
+ }
82
+
83
+ /* Snapped to whole lights, so none is split between two colours */
84
+ :is(.sonic-meter-segments, .sonic-meter-level) {
85
+ --_sonic-meter-span: calc(var(--_sonic-meter-max, 0) - var(--_sonic-meter-min, -60));
86
+ --_sonic-meter-hot-at: round(
87
+ clamp(
88
+ 0,
89
+ (var(--_sonic-meter-hot-from) - var(--_sonic-meter-min, -60)) / var(--_sonic-meter-span),
90
+ 1
91
+ ) *
92
+ var(--_sonic-meter-segments),
93
+ var(--_sonic-meter-pitch)
94
+ );
95
+ --_sonic-meter-clip-at: round(
96
+ clamp(
97
+ 0,
98
+ (var(--_sonic-meter-clip-from) - var(--_sonic-meter-min, -60)) / var(--_sonic-meter-span),
99
+ 1
100
+ ) *
101
+ var(--_sonic-meter-segments),
102
+ var(--_sonic-meter-pitch)
103
+ );
104
+
105
+ /* Each rounded light is two crossed bands and four corner dots added together */
106
+ --_sonic-meter-lights:
107
+ linear-gradient(to top, #000 var(--_sonic-meter-segment), transparent 0)
108
+ var(--_sonic-meter-segment-radius) 100% /
109
+ calc(100% - 2 * var(--_sonic-meter-segment-radius)) var(--_sonic-meter-pitch) no-repeat
110
+ repeat,
111
+ linear-gradient(
112
+ to top,
113
+ transparent var(--_sonic-meter-segment-radius),
114
+ #000 0 calc(var(--_sonic-meter-segment) - var(--_sonic-meter-segment-radius)),
115
+ transparent 0
116
+ )
117
+ 0 100% / 100% var(--_sonic-meter-pitch) no-repeat repeat,
118
+ radial-gradient(
119
+ circle var(--_sonic-meter-segment-radius) at left var(--_sonic-meter-segment-radius)
120
+ bottom var(--_sonic-meter-segment-radius),
121
+ #000 calc(100% - 0.5px),
122
+ transparent
123
+ )
124
+ 0 100% / 100% var(--_sonic-meter-pitch) no-repeat repeat,
125
+ radial-gradient(
126
+ circle var(--_sonic-meter-segment-radius) at right var(--_sonic-meter-segment-radius)
127
+ bottom var(--_sonic-meter-segment-radius),
128
+ #000 calc(100% - 0.5px),
129
+ transparent
130
+ )
131
+ 0 100% / 100% var(--_sonic-meter-pitch) no-repeat repeat,
132
+ radial-gradient(
133
+ circle var(--_sonic-meter-segment-radius) at left var(--_sonic-meter-segment-radius)
134
+ bottom calc(var(--_sonic-meter-segment) - var(--_sonic-meter-segment-radius)),
135
+ #000 calc(100% - 0.5px),
136
+ transparent
137
+ )
138
+ 0 100% / 100% var(--_sonic-meter-pitch) no-repeat repeat,
139
+ radial-gradient(
140
+ circle var(--_sonic-meter-segment-radius) at right var(--_sonic-meter-segment-radius)
141
+ bottom calc(var(--_sonic-meter-segment) - var(--_sonic-meter-segment-radius)),
142
+ #000 calc(100% - 0.5px),
143
+ transparent
144
+ )
145
+ 0 100% / 100% var(--_sonic-meter-pitch) no-repeat repeat;
146
+
147
+ position: absolute;
148
+ inset: auto var(--_sonic-meter-inset) var(--_sonic-meter-inset);
149
+ block-size: calc(var(--_sonic-meter-segments) - var(--_sonic-meter-gap));
150
+ mask: var(--_sonic-meter-lights);
151
+ mask-composite: add;
152
+ }
153
+
154
+ [orientation='horizontal'] > .sonic-meter > :is(.sonic-meter-segments, .sonic-meter-level) {
155
+ --_sonic-meter-lights:
156
+ linear-gradient(to right, #000 var(--_sonic-meter-segment), transparent 0) 0
157
+ var(--_sonic-meter-segment-radius) / var(--_sonic-meter-pitch)
158
+ calc(100% - 2 * var(--_sonic-meter-segment-radius)) repeat no-repeat,
159
+ linear-gradient(
160
+ to right,
161
+ transparent var(--_sonic-meter-segment-radius),
162
+ #000 0 calc(var(--_sonic-meter-segment) - var(--_sonic-meter-segment-radius)),
163
+ transparent 0
164
+ )
165
+ 0 0 / var(--_sonic-meter-pitch) 100% repeat no-repeat,
166
+ radial-gradient(
167
+ circle var(--_sonic-meter-segment-radius) at left var(--_sonic-meter-segment-radius) top
168
+ var(--_sonic-meter-segment-radius),
169
+ #000 calc(100% - 0.5px),
170
+ transparent
171
+ )
172
+ 0 0 / var(--_sonic-meter-pitch) 100% repeat no-repeat,
173
+ radial-gradient(
174
+ circle var(--_sonic-meter-segment-radius) at left var(--_sonic-meter-segment-radius)
175
+ /* fallow-ignore-next-line code-duplication -- one gradient per segment corner is geometry, not copied logic */
176
+ bottom var(--_sonic-meter-segment-radius),
177
+ #000 calc(100% - 0.5px),
178
+ transparent
179
+ )
180
+ 0 0 / var(--_sonic-meter-pitch) 100% repeat no-repeat,
181
+ radial-gradient(
182
+ circle var(--_sonic-meter-segment-radius) at left
183
+ calc(var(--_sonic-meter-segment) - var(--_sonic-meter-segment-radius)) top
184
+ var(--_sonic-meter-segment-radius),
185
+ #000 calc(100% - 0.5px),
186
+ transparent
187
+ )
188
+ 0 0 / var(--_sonic-meter-pitch) 100% repeat no-repeat,
189
+ radial-gradient(
190
+ circle var(--_sonic-meter-segment-radius) at left
191
+ calc(var(--_sonic-meter-segment) - var(--_sonic-meter-segment-radius)) bottom
192
+ var(--_sonic-meter-segment-radius),
193
+ #000 calc(100% - 0.5px),
194
+ transparent
195
+ )
196
+ 0 0 / var(--_sonic-meter-pitch) 100% repeat no-repeat;
197
+
198
+ inset: var(--_sonic-meter-inset) auto var(--_sonic-meter-inset) var(--_sonic-meter-inset);
199
+ inline-size: calc(var(--_sonic-meter-segments) - var(--_sonic-meter-gap));
200
+ block-size: auto;
201
+ }
202
+
203
+ .sonic-meter-segments {
204
+ background: linear-gradient(
205
+ var(--_sonic-meter-toward),
206
+ color-mix(in oklab, var(--_sonic-lit) 12%, var(--_sonic-unlit)) 0 var(--_sonic-meter-hot-at),
207
+ color-mix(in oklab, var(--_sonic-hot) 12%, var(--_sonic-unlit)) 0 var(--_sonic-meter-clip-at),
208
+ color-mix(in oklab, var(--_sonic-clip) 12%, var(--_sonic-unlit)) 0
209
+ );
210
+ }
211
+
212
+ /* One mask for the bar and peak, so a frame repaints without layout */
213
+ .sonic-meter-level {
214
+ --_sonic-meter-lit: round(
215
+ down,
216
+ var(--_sonic-meter-level, 0) * var(--_sonic-meter-segments),
217
+ var(--_sonic-meter-pitch)
218
+ );
219
+ --_sonic-meter-held: round(
220
+ down,
221
+ var(--_sonic-meter-peak, 0) * var(--_sonic-meter-segments),
222
+ var(--_sonic-meter-pitch)
223
+ );
224
+
225
+ background: linear-gradient(
226
+ var(--_sonic-meter-toward),
227
+ var(--_sonic-lit) 0 var(--_sonic-meter-hot-at),
228
+ var(--_sonic-hot) 0 var(--_sonic-meter-clip-at),
229
+ var(--_sonic-clip) 0
230
+ );
231
+ mask:
232
+ linear-gradient(
233
+ var(--_sonic-meter-toward),
234
+ #000 var(--_sonic-meter-lit),
235
+ transparent 0 calc(var(--_sonic-meter-held) - var(--_sonic-meter-pitch)),
236
+ #000 0 var(--_sonic-meter-held),
237
+ transparent 0
238
+ ),
239
+ var(--_sonic-meter-lights);
240
+ mask-composite: intersect, add, add, add, add, add, add;
241
+ }
242
+
243
+ .sonic-meter-clip {
244
+ position: absolute;
245
+ inset: var(--_sonic-meter-inset) var(--_sonic-meter-inset) auto;
246
+ block-size: var(--_sonic-meter-lens);
247
+ background:
248
+ linear-gradient(var(--_sonic-light-fall), rgb(255 255 255 / 10%), transparent 60%),
249
+ color-mix(in oklab, var(--_sonic-unlit) 60%, #000);
250
+ border-radius: 50%;
251
+
252
+ &::after {
253
+ position: absolute;
254
+ inset: 0;
255
+ content: '';
256
+ background: radial-gradient(
257
+ closest-side,
258
+ color-mix(in oklab, var(--_sonic-clip) 55%, #fff),
259
+ var(--_sonic-clip)
260
+ );
261
+ border-radius: inherit;
262
+ opacity: var(--_sonic-meter-clipped, 0);
263
+ }
264
+ }
265
+
266
+ [orientation='horizontal'] > .sonic-meter > .sonic-meter-clip {
267
+ inset: var(--_sonic-meter-inset) var(--_sonic-meter-inset) var(--_sonic-meter-inset) auto;
268
+ inline-size: var(--_sonic-meter-lens);
269
+ block-size: auto;
270
+ }
271
+ }
@@ -0,0 +1,47 @@
1
+ @layer sonic {
2
+ .sonic-screen {
3
+ --_sonic-screen-size: var(--sonic-screen-size, 2.5rem);
4
+ --_sonic-screen-aspect-ratio: var(--sonic-screen-aspect-ratio, 3);
5
+ --_sonic-screen-radius-ratio: var(--sonic-screen-radius-ratio, 0.15);
6
+ --_sonic-screen-inset-ratio: var(--sonic-screen-inset-ratio, 0.1);
7
+ --_sonic-unit: calc(var(--_sonic-screen-size) / 32);
8
+
9
+ position: relative;
10
+ box-sizing: border-box;
11
+ display: grid;
12
+ flex-shrink: 0;
13
+
14
+ /* A definite cell, so percentage-sized content fills the glass; a canvas sized to its own box would feed back */
15
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
16
+ place-items: center;
17
+ inline-size: calc(var(--_sonic-screen-size) * var(--_sonic-screen-aspect-ratio));
18
+ block-size: var(--_sonic-screen-size);
19
+ padding: calc(var(--_sonic-screen-size) * var(--_sonic-screen-inset-ratio));
20
+
21
+ /* Not the cap's ink, which a skin may darken */
22
+ color: color-mix(in oklab, var(--_sonic-glass) 20%, #fff);
23
+
24
+ /* A double click aimed at a neighbouring control would highlight it */
25
+ user-select: none;
26
+ background: radial-gradient(
27
+ closest-side,
28
+ color-mix(in oklab, var(--_sonic-glass) 88%, #fff),
29
+ var(--_sonic-glass)
30
+ )
31
+ var(--_sonic-glass);
32
+ border-radius: calc(var(--_sonic-screen-size) * var(--_sonic-screen-radius-ratio));
33
+
34
+ /* Over the content, so the lip shades the picture too */
35
+ &::after {
36
+ position: absolute;
37
+ inset: 0;
38
+ pointer-events: none;
39
+ content: '';
40
+ background: linear-gradient(var(--_sonic-light-fall), rgb(255 255 255 / 7%), transparent 45%);
41
+ border-radius: inherit;
42
+ box-shadow:
43
+ var(--_sonic-well),
44
+ inset 0 0 calc(3 * var(--_sonic-unit)) rgb(0 0 0 / 45%);
45
+ }
46
+ }
47
+ }