@xsynaptic/sonic-ui 0.3.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 (41) hide show
  1. package/dist/default/elements/dial.js +5 -3
  2. package/dist/default/elements/key.js +29 -4
  3. package/dist/default/elements/meter.js +63 -9
  4. package/dist/default/elements/number.js +1 -1
  5. package/dist/default/elements/range-element.js +134 -21
  6. package/dist/default/elements/slider.js +49 -3
  7. package/dist/default/lib/detent.js +17 -0
  8. package/dist/default/lib/math.js +16 -0
  9. package/dist/default/lib/number-list.js +14 -0
  10. package/dist/default/lib/taper.js +17 -7
  11. package/dist/dev/elements/dial.js +5 -3
  12. package/dist/dev/elements/dial.js.map +1 -1
  13. package/dist/dev/elements/key.d.ts +4 -0
  14. package/dist/dev/elements/key.js +29 -4
  15. package/dist/dev/elements/key.js.map +1 -1
  16. package/dist/dev/elements/meter.d.ts +6 -0
  17. package/dist/dev/elements/meter.js +63 -9
  18. package/dist/dev/elements/meter.js.map +1 -1
  19. package/dist/dev/elements/number.js +1 -1
  20. package/dist/dev/elements/range-element.d.ts +8 -0
  21. package/dist/dev/elements/range-element.js +134 -21
  22. package/dist/dev/elements/range-element.js.map +1 -1
  23. package/dist/dev/elements/slider.d.ts +6 -0
  24. package/dist/dev/elements/slider.js +49 -3
  25. package/dist/dev/elements/slider.js.map +1 -1
  26. package/dist/dev/lib/detent.js +18 -0
  27. package/dist/dev/lib/detent.js.map +1 -0
  28. package/dist/dev/lib/math.js +17 -0
  29. package/dist/dev/lib/math.js.map +1 -0
  30. package/dist/dev/lib/number-list.js +15 -0
  31. package/dist/dev/lib/number-list.js.map +1 -0
  32. package/dist/dev/lib/taper.js +17 -7
  33. package/dist/dev/lib/taper.js.map +1 -1
  34. package/dist/styles/dial.css +43 -3
  35. package/dist/styles/key.css +60 -15
  36. package/dist/styles/led.css +8 -1
  37. package/dist/styles/material.css +50 -1
  38. package/dist/styles/meter.css +101 -5
  39. package/dist/styles/segmented.css +2 -7
  40. package/dist/styles/slider.css +94 -32
  41. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"slider.js","names":[],"sources":["../../../src/elements/slider.ts"],"sourcesContent":["import type { RangeAxis } from '#elements/range-element.ts';\n\nimport { SonicRangeElement } from '#elements/range-element.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-slider': SonicSlider;\n\t}\n}\n\ninterface SliderAxis extends RangeAxis {\n\t// The cap's centre at the minimum, on the same scale as `position`\n\tstartPx: number;\n}\n\nconst renderSlider = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-slider\" role=\"slider\" tabindex=\"0\">\n\t\t\t<div class=\"sonic-slider-marks\"></div>\n\t\t\t<div class=\"sonic-slider-groove\"></div>\n\t\t\t<div class=\"sonic-slider-modulation\"></div>\n\t\t\t<div class=\"sonic-slider-cap\"></div>\n\t\t\t<div class=\"sonic-slider-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-slider-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nexport class SonicSlider extends SonicRangeElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicRangeElement.observedAttributes,\n\t\t'modulation',\n\t\t'orientation',\n\t];\n\n\t// Named for the absent attribute, as `input.type` reads `text`\n\tget groovePress(): 'jump' | 'none' {\n\t\treturn this.getAttribute('groove-press') === 'none' ? 'none' : 'jump';\n\t}\n\n\tset groovePress(gesture: 'jump' | 'none' | undefined) {\n\t\tthis.reflect('groove-press', gesture);\n\t}\n\n\tget modulation(): number {\n\t\treturn this.numberAttribute('modulation', 0);\n\t}\n\n\tset modulation(amount: number | undefined) {\n\t\tthis.reflect('modulation', amount);\n\t}\n\n\tget orientation(): 'horizontal' | 'vertical' {\n\t\treturn this.getAttribute('orientation') === 'vertical' ? 'vertical' : 'horizontal';\n\t}\n\n\tset orientation(direction: 'horizontal' | 'vertical' | undefined) {\n\t\tthis.reflect('orientation', direction);\n\t}\n\n\treadonly #slider = renderSlider();\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('groovePress', 'modulation', 'orientation');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst slider = this.#slider;\n\t\tconst cap = requireChild(slider, '.sonic-slider-cap', HTMLDivElement);\n\n\t\tthis.appendOnce(slider);\n\t\tthis.render();\n\t\tthis.checkStyles(slider, 'slider.css');\n\t\tthis.bindGestures(slider, signal, (event) => {\n\t\t\tconst axis = this.#axis(slider, cap);\n\t\t\tif (event.target instanceof Node && cap.contains(event.target)) return axis;\n\n\t\t\tif (this.groovePress === 'none') return;\n\n\t\t\tthis.input(this.valueAt((axis.position(event) - axis.startPx) / axis.travelPx));\n\n\t\t\treturn axis;\n\t\t});\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#slider;\n\t}\n\n\tprotected render(): void {\n\t\tconst slider = this.#slider;\n\t\tconst positions = this.positions();\n\t\tconst [modulationFrom, modulationTo] = this.modulationFractions(this.modulation);\n\n\t\tslider.style.setProperty('--_sonic-slider-value', String(this.fraction(this.value)));\n\t\tslider.style.setProperty('--_sonic-slider-origin', String(this.originFraction()));\n\t\tslider.style.setProperty('--_sonic-slider-modulation-from', String(modulationFrom));\n\t\tslider.style.setProperty('--_sonic-slider-modulation-to', String(modulationTo));\n\t\tif (positions === undefined) slider.style.removeProperty('--_sonic-slider-positions');\n\t\telse slider.style.setProperty('--_sonic-slider-positions', String(positions));\n\t\tthis.renderAria(slider, this.orientation);\n\t}\n\n\t#axis(slider: HTMLElement, cap: HTMLElement): SliderAxis {\n\t\tconst track = slider.getBoundingClientRect();\n\t\tconst capBox = cap.getBoundingClientRect();\n\n\t\tif (this.orientation === 'vertical') {\n\t\t\treturn {\n\t\t\t\tposition: (event) => -event.clientY,\n\t\t\t\tstartPx: capBox.height / 2 - track.bottom,\n\t\t\t\ttravelPx: Math.max(1, track.height - capBox.height),\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tposition: (event) => event.clientX,\n\t\t\tstartPx: track.left + capBox.width / 2,\n\t\t\ttravelPx: Math.max(1, track.width - capBox.width),\n\t\t};\n\t}\n}\n"],"mappings":";;;;AAgBA,MAAM,eAAe,SACT;;;;;;;;;;;;;;;;;;IAmBX,cACD;AAEA,IAAa,cAAb,cAAiC,kBAAkB;CAClD,OAAyB,qBAAqB;EAC7C,GAAG,kBAAkB;EACrB;EACA;CACD;CAGA,IAAI,cAA+B;EAClC,OAAO,KAAK,aAAa,cAAc,MAAM,SAAS,SAAS;CAChE;CAEA,IAAI,YAAY,SAAsC;EACrD,KAAK,QAAQ,gBAAgB,OAAO;CACrC;CAEA,IAAI,aAAqB;EACxB,OAAO,KAAK,gBAAgB,cAAc,CAAC;CAC5C;CAEA,IAAI,WAAW,QAA4B;EAC1C,KAAK,QAAQ,cAAc,MAAM;CAClC;CAEA,IAAI,cAAyC;EAC5C,OAAO,KAAK,aAAa,aAAa,MAAM,aAAa,aAAa;CACvE;CAEA,IAAI,YAAY,WAAkD;EACjE,KAAK,QAAQ,eAAe,SAAS;CACtC;CAEA,AAAS,UAAU,aAAa;CAEhC,AAAS,oBAA0B;EAClC,KAAK,kBAAkB,eAAe,cAAc,aAAa;EACjE,MAAM,kBAAkB;CACzB;CAEA,AAAU,QAAQ,QAA2B;EAC5C,MAAM,SAAS,KAAK;EACpB,MAAM,MAAM,aAAa,QAAQ,qBAAqB,cAAc;EAEpE,KAAK,WAAW,MAAM;EACtB,KAAK,OAAO;EACZ,KAAK,YAAY,QAAQ,YAAY;EACrC,KAAK,aAAa,QAAQ,SAAS,UAAU;GAC5C,MAAM,OAAO,KAAK,MAAM,QAAQ,GAAG;GACnC,IAAI,MAAM,kBAAkB,QAAQ,IAAI,SAAS,MAAM,MAAM,GAAG,OAAO;GAEvE,IAAI,KAAK,gBAAgB,QAAQ;GAEjC,KAAK,MAAM,KAAK,SAAS,KAAK,SAAS,KAAK,IAAI,KAAK,WAAW,KAAK,QAAQ,CAAC;GAE9E,OAAO;EACR,CAAC;CACF;CAEA,AAAmB,cAA2B;EAC7C,OAAO,KAAK;CACb;CAEA,AAAU,SAAe;EACxB,MAAM,SAAS,KAAK;EACpB,MAAM,YAAY,KAAK,UAAU;EACjC,MAAM,CAAC,gBAAgB,gBAAgB,KAAK,oBAAoB,KAAK,UAAU;EAE/E,OAAO,MAAM,YAAY,yBAAyB,OAAO,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC;EACnF,OAAO,MAAM,YAAY,0BAA0B,OAAO,KAAK,eAAe,CAAC,CAAC;EAChF,OAAO,MAAM,YAAY,mCAAmC,OAAO,cAAc,CAAC;EAClF,OAAO,MAAM,YAAY,iCAAiC,OAAO,YAAY,CAAC;EAC9E,IAAI,cAAc,QAAW,OAAO,MAAM,eAAe,2BAA2B;OAC/E,OAAO,MAAM,YAAY,6BAA6B,OAAO,SAAS,CAAC;EAC5E,KAAK,WAAW,QAAQ,KAAK,WAAW;CACzC;CAEA,MAAM,QAAqB,KAA8B;EACxD,MAAM,QAAQ,OAAO,sBAAsB;EAC3C,MAAM,SAAS,IAAI,sBAAsB;EAEzC,IAAI,KAAK,gBAAgB,YACxB,OAAO;GACN,WAAW,UAAU,CAAC,MAAM;GAC5B,SAAS,OAAO,SAAS,IAAI,MAAM;GACnC,UAAU,KAAK,IAAI,GAAG,MAAM,SAAS,OAAO,MAAM;EACnD;EAGD,OAAO;GACN,WAAW,UAAU,MAAM;GAC3B,SAAS,MAAM,OAAO,OAAO,QAAQ;GACrC,UAAU,KAAK,IAAI,GAAG,MAAM,QAAQ,OAAO,KAAK;EACjD;CACD;AACD"}
1
+ {"version":3,"file":"slider.js","names":[],"sources":["../../../src/elements/slider.ts"],"sourcesContent":["import type { RangeAxis } from '#elements/range-element.ts';\n\nimport { SonicRangeElement } from '#elements/range-element.ts';\nimport { requireChild, template } from '#lib/render.ts';\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'sonic-slider': SonicSlider;\n\t}\n}\n\ninterface SliderAxis extends RangeAxis {\n\t// The cap's centre at the minimum, on the same scale as `position`\n\tstartPx: number;\n}\n\nexport type BufferedRanges = Iterable<readonly [number, number]> | TimeRanges;\n\nfunction readRanges(ranges: BufferedRanges): Array<[number, number]> {\n\tif ('start' in ranges) {\n\t\treturn Array.from({ length: ranges.length }, (_entry, index) => [\n\t\t\tranges.start(index),\n\t\t\tranges.end(index),\n\t\t]);\n\t}\n\n\treturn [...ranges].map(([start, end]) => [start, end]);\n}\n\n// Along the groove, which runs past the travel by half its width at each end\nfunction grooveStop(fraction: number): string {\n\tif (fraction <= 0) return '0%';\n\tif (fraction >= 1) return '100%';\n\n\treturn `calc(${String(fraction)} * var(--_sonic-slider-travel) + var(--_sonic-slider-groove) / 2)`;\n}\n\nconst renderSlider = template(\n\t/* HTML */ `\n\t\t<div class=\"sonic-slider\" role=\"slider\" tabindex=\"0\">\n\t\t\t<div class=\"sonic-slider-scale\" aria-hidden=\"true\"></div>\n\t\t\t<div class=\"sonic-slider-marks\"></div>\n\t\t\t<div class=\"sonic-slider-groove\"></div>\n\t\t\t<div class=\"sonic-slider-modulation\"></div>\n\t\t\t<div class=\"sonic-slider-cap\"></div>\n\t\t\t<div class=\"sonic-slider-readout\" popover=\"manual\">\n\t\t\t\t<span></span>\n\t\t\t\t<input\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tclass=\"sonic-slider-entry\"\n\t\t\t\t\tenterkeyhint=\"done\"\n\t\t\t\t\thidden\n\t\t\t\t\tspellcheck=\"false\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t`,\n\tHTMLDivElement,\n);\n\nexport class SonicSlider extends SonicRangeElement {\n\tstatic override readonly observedAttributes = [\n\t\t...SonicRangeElement.observedAttributes,\n\t\t'modulation',\n\t\t'orientation',\n\t\t'spring',\n\t];\n\n\tget buffered(): Array<[number, number]> {\n\t\treturn this.#buffered.map(([start, end]) => [start, end]);\n\t}\n\n\tset buffered(ranges: BufferedRanges | undefined) {\n\t\tthis.#buffered = ranges\n\t\t\t? readRanges(ranges).toSorted((first, second) => first[0] - second[0])\n\t\t\t: [];\n\t\tthis.render();\n\t}\n\n\t// Named for the absent attribute, as `input.type` reads `text`\n\tget groovePress(): 'jump' | 'none' {\n\t\treturn this.getAttribute('groove-press') === 'none' ? 'none' : 'jump';\n\t}\n\n\tset groovePress(gesture: 'jump' | 'none' | undefined) {\n\t\tthis.reflect('groove-press', gesture);\n\t}\n\n\tget modulation(): number {\n\t\treturn this.numberAttribute('modulation', 0);\n\t}\n\n\tset modulation(amount: number | undefined) {\n\t\tthis.reflect('modulation', amount);\n\t}\n\n\tget orientation(): 'horizontal' | 'vertical' {\n\t\treturn this.getAttribute('orientation') === 'vertical' ? 'vertical' : 'horizontal';\n\t}\n\n\tset orientation(direction: 'horizontal' | 'vertical' | undefined) {\n\t\tthis.reflect('orientation', direction);\n\t}\n\n\tget spring(): boolean {\n\t\treturn this.hasAttribute('spring');\n\t}\n\n\tset spring(isSpring: boolean) {\n\t\tthis.reflect('spring', isSpring);\n\t}\n\n\t#buffered: Array<[number, number]> = [];\n\n\treadonly #slider = renderSlider();\n\n\toverride connectedCallback(): void {\n\t\tthis.upgradeProperties('buffered', 'groovePress', 'modulation', 'orientation', 'spring');\n\t\tsuper.connectedCallback();\n\t}\n\n\tprotected connect(signal: AbortSignal): void {\n\t\tconst slider = this.#slider;\n\t\tconst cap = requireChild(slider, '.sonic-slider-cap', HTMLDivElement);\n\n\t\tthis.bindScale(slider, requireChild(slider, '.sonic-slider-scale', HTMLDivElement), signal);\n\t\tthis.render();\n\t\tthis.checkStyles(slider, 'slider.css');\n\t\tthis.bindGestures(slider, signal, (event) => {\n\t\t\tconst axis = this.#axis(slider, cap);\n\t\t\tif (event.target instanceof Node && cap.contains(event.target)) return axis;\n\n\t\t\tif (this.groovePress === 'none') return;\n\n\t\t\tthis.input(this.valueAt((axis.position(event) - axis.startPx) / axis.travelPx));\n\n\t\t\treturn axis;\n\t\t});\n\t}\n\n\tprotected override focusTarget(): HTMLElement {\n\t\treturn this.#slider;\n\t}\n\n\tprotected render(): void {\n\t\tconst slider = this.#slider;\n\t\tconst positions = this.positions();\n\t\tconst [modulationFrom, modulationTo] = this.modulationFractions(this.modulation);\n\n\t\tslider.style.setProperty('--_sonic-slider-value', String(this.fraction(this.value)));\n\t\tslider.style.setProperty('--_sonic-slider-origin', String(this.originFraction()));\n\t\tslider.style.setProperty('--_sonic-slider-modulation-from', String(modulationFrom));\n\t\tslider.style.setProperty('--_sonic-slider-modulation-to', String(modulationTo));\n\t\tif (positions === undefined) slider.style.removeProperty('--_sonic-slider-positions');\n\t\telse slider.style.setProperty('--_sonic-slider-positions', String(positions));\n\t\tthis.#renderBuffered(slider);\n\t\tthis.renderScale();\n\t\tthis.renderAria(slider, this.orientation);\n\t}\n\n\tprotected override springTarget(): number | undefined {\n\t\treturn this.spring ? this.restValue() : undefined;\n\t}\n\n\t#axis(slider: HTMLElement, cap: HTMLElement): SliderAxis {\n\t\tconst track = slider.getBoundingClientRect();\n\t\tconst capBox = cap.getBoundingClientRect();\n\n\t\tif (this.orientation === 'vertical') {\n\t\t\treturn {\n\t\t\t\tposition: (event) => -event.clientY,\n\t\t\t\tstartPx: capBox.height / 2 - track.bottom,\n\t\t\t\ttravelPx: Math.max(1, track.height - capBox.height),\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tposition: (event) => event.clientX,\n\t\t\tstartPx: track.left + capBox.width / 2,\n\t\t\ttravelPx: Math.max(1, track.width - capBox.width),\n\t\t};\n\t}\n\n\t// Hard stops in one gradient, so any number of ranges costs one layer\n\t#renderBuffered(slider: HTMLElement): void {\n\t\tif (this.#buffered.length === 0) {\n\t\t\tslider.style.removeProperty('--_sonic-slider-buffered-ranges');\n\t\t\treturn;\n\t\t}\n\n\t\tconst stops = this.#buffered.flatMap(([start, end]) => {\n\t\t\tconst from = grooveStop(this.fraction(Math.min(start, end)));\n\t\t\tconst to = grooveStop(this.fraction(Math.max(start, end)));\n\n\t\t\treturn [\n\t\t\t\t`transparent ${from}`,\n\t\t\t\t`var(--_sonic-slider-buffered) ${from} ${to}`,\n\t\t\t\t`transparent ${to}`,\n\t\t\t];\n\t\t});\n\n\t\tslider.style.setProperty(\n\t\t\t'--_sonic-slider-buffered-ranges',\n\t\t\t`linear-gradient(var(--_sonic-slider-toward), ${stops.join(', ')})`,\n\t\t);\n\t}\n}\n"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ //#region src/lib/detent.ts
2
+ function isAcross(from, to, place) {
3
+ return from !== place && (from - place) * (to - place) <= 0;
4
+ }
5
+ function passDetent(hold, from, to) {
6
+ if (hold.slack === void 0 && !isAcross(from, to, hold.place)) return to;
7
+ const slack = (hold.slack ?? 0) + to - hold.place;
8
+ if (Math.abs(slack) <= hold.zone) {
9
+ hold.slack = slack;
10
+ return;
11
+ }
12
+ hold.slack = void 0;
13
+ return hold.place + slack - Math.sign(slack) * hold.zone;
14
+ }
15
+
16
+ //#endregion
17
+ export { passDetent };
18
+ //# sourceMappingURL=detent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detent.js","names":[],"sources":["../../../src/lib/detent.ts"],"sourcesContent":["// Places run 0 to 1 along the travel; `slack` is how far past the detent the pointer has pushed, set while held\nexport interface DetentHold {\n\tplace: number;\n\tslack: number | undefined;\n\tvalue: number;\n\tzone: number;\n}\n\nfunction isAcross(from: number, to: number, place: number): boolean {\n\treturn from !== place && (from - place) * (to - place) <= 0;\n}\n\n// The next place along the travel, or `undefined` while the detent holds\nexport function passDetent(hold: DetentHold, from: number, to: number): number | undefined {\n\tif (hold.slack === undefined && !isAcross(from, to, hold.place)) return to;\n\n\t// Held, `from` is the detent's place, so the move adds to the slack\n\tconst slack = (hold.slack ?? 0) + to - hold.place;\n\n\tif (Math.abs(slack) <= hold.zone) {\n\t\thold.slack = slack;\n\t\treturn undefined;\n\t}\n\n\thold.slack = undefined;\n\n\treturn hold.place + slack - Math.sign(slack) * hold.zone;\n}\n"],"mappings":";AAQA,SAAS,SAAS,MAAc,IAAY,OAAwB;CACnE,OAAO,SAAS,UAAU,OAAO,UAAU,KAAK,UAAU;AAC3D;AAGA,SAAgB,WAAW,MAAkB,MAAc,IAAgC;CAC1F,IAAI,KAAK,UAAU,UAAa,CAAC,SAAS,MAAM,IAAI,KAAK,KAAK,GAAG,OAAO;CAGxE,MAAM,SAAS,KAAK,SAAS,KAAK,KAAK,KAAK;CAE5C,IAAI,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM;EACjC,KAAK,QAAQ;EACb;CACD;CAEA,KAAK,QAAQ;CAEb,OAAO,KAAK,QAAQ,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK;AACrD"}
@@ -0,0 +1,17 @@
1
+ //#region src/lib/math.ts
2
+ function clamp(value, min, max) {
3
+ return Math.min(max, Math.max(min, value));
4
+ }
5
+ function clampUnit(fraction) {
6
+ return clamp(fraction, 0, 1);
7
+ }
8
+ function wrapUnit(fraction) {
9
+ return fraction - Math.floor(fraction);
10
+ }
11
+ function trimFloat(value) {
12
+ return Number(value.toPrecision(12));
13
+ }
14
+
15
+ //#endregion
16
+ export { clamp, clampUnit, trimFloat, wrapUnit };
17
+ //# sourceMappingURL=math.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"math.js","names":[],"sources":["../../../src/lib/math.ts"],"sourcesContent":["export function clamp(value: number, min: number, max: number): number {\n\treturn Math.min(max, Math.max(min, value));\n}\n\nexport function clampUnit(fraction: number): number {\n\treturn clamp(fraction, 0, 1);\n}\n\nexport function wrapUnit(fraction: number): number {\n\treturn fraction - Math.floor(fraction);\n}\n\nexport function trimFloat(value: number): number {\n\treturn Number(value.toPrecision(12));\n}\n"],"mappings":";AAAA,SAAgB,MAAM,OAAe,KAAa,KAAqB;CACtE,OAAO,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC1C;AAEA,SAAgB,UAAU,UAA0B;CACnD,OAAO,MAAM,UAAU,GAAG,CAAC;AAC5B;AAEA,SAAgB,SAAS,UAA0B;CAClD,OAAO,WAAW,KAAK,MAAM,QAAQ;AACtC;AAEA,SAAgB,UAAU,OAAuB;CAChD,OAAO,OAAO,MAAM,YAAY,EAAE,CAAC;AACpC"}
@@ -0,0 +1,15 @@
1
+ //#region src/lib/number-list.ts
2
+ function parseNumberList(text) {
3
+ if (text === null) return void 0;
4
+ const entries = [...new Set(text.split(/[\s,]+/).filter(Boolean).map(Number).filter((entry) => Number.isFinite(entry)))].toSorted((first, second) => first - second);
5
+ return entries.length >= 2 ? entries : void 0;
6
+ }
7
+ function nearestEntry(entries, value) {
8
+ let nearest = entries[0] ?? value;
9
+ for (const entry of entries) if (Math.abs(entry - value) < Math.abs(nearest - value)) nearest = entry;
10
+ return nearest;
11
+ }
12
+
13
+ //#endregion
14
+ export { nearestEntry, parseNumberList };
15
+ //# sourceMappingURL=number-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"number-list.js","names":[],"sources":["../../../src/lib/number-list.ts"],"sourcesContent":["export function parseNumberList(text: null | string): Array<number> | undefined {\n\tif (text === null) return undefined;\n\n\tconst entries = [\n\t\t...new Set(\n\t\t\ttext\n\t\t\t\t.split(/[\\s,]+/)\n\t\t\t\t.filter(Boolean)\n\t\t\t\t.map(Number)\n\t\t\t\t.filter((entry) => Number.isFinite(entry)),\n\t\t),\n\t].toSorted((first, second) => first - second);\n\n\treturn entries.length >= 2 ? entries : undefined;\n}\n\nexport function nearestEntry(entries: Array<number>, value: number): number {\n\tlet nearest = entries[0] ?? value;\n\n\tfor (const entry of entries) {\n\t\tif (Math.abs(entry - value) < Math.abs(nearest - value)) nearest = entry;\n\t}\n\n\treturn nearest;\n}\n"],"mappings":";AAAA,SAAgB,gBAAgB,MAAgD;CAC/E,IAAI,SAAS,MAAM,OAAO;CAE1B,MAAM,UAAU,CACf,GAAG,IAAI,IACN,KACE,MAAM,QAAQ,CAAC,CACf,OAAO,OAAO,CAAC,CACf,IAAI,MAAM,CAAC,CACX,QAAQ,UAAU,OAAO,SAAS,KAAK,CAAC,CAC3C,CACD,CAAC,CAAC,UAAU,OAAO,WAAW,QAAQ,MAAM;CAE5C,OAAO,QAAQ,UAAU,IAAI,UAAU;AACxC;AAEA,SAAgB,aAAa,SAAwB,OAAuB;CAC3E,IAAI,UAAU,QAAQ,MAAM;CAE5B,KAAK,MAAM,SAAS,SACnB,IAAI,KAAK,IAAI,QAAQ,KAAK,IAAI,KAAK,IAAI,UAAU,KAAK,GAAG,UAAU;CAGpE,OAAO;AACR"}
@@ -1,10 +1,6 @@
1
+ import { clampUnit } from "./math.js";
2
+
1
3
  //#region src/lib/taper.ts
2
- function clampUnit(fraction) {
3
- return Math.min(1, Math.max(0, fraction));
4
- }
5
- function wrapUnit(fraction) {
6
- return fraction - Math.floor(fraction);
7
- }
8
4
  function linearTaper(min, max) {
9
5
  const range = max - min;
10
6
  return {
@@ -30,7 +26,21 @@ function skewTaper(min, max, midpoint) {
30
26
  value: (position) => min + range * clampUnit(position) ** (1 / skew)
31
27
  };
32
28
  }
29
+ function listTaper(entries) {
30
+ const last = entries.length - 1;
31
+ return {
32
+ position: (value) => {
33
+ const above = entries.findIndex((entry) => entry >= value);
34
+ if (above === -1) return 1;
35
+ const high = entries[above];
36
+ const low = entries[above - 1];
37
+ if (high === void 0 || low === void 0) return 0;
38
+ return (above - 1 + (value - low) / (high - low)) / last;
39
+ },
40
+ value: (position) => entries[Math.round(clampUnit(position) * last)] ?? NaN
41
+ };
42
+ }
33
43
 
34
44
  //#endregion
35
- export { clampUnit, linearTaper, logTaper, skewTaper, wrapUnit };
45
+ export { linearTaper, listTaper, logTaper, skewTaper };
36
46
  //# sourceMappingURL=taper.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"taper.js","names":[],"sources":["../../../src/lib/taper.ts"],"sourcesContent":["// Positions run 0 to 1 along the travel\nexport interface Taper {\n\tposition: (value: number) => number;\n\tvalue: (position: number) => number;\n}\n\nexport function clampUnit(fraction: number): number {\n\treturn Math.min(1, Math.max(0, fraction));\n}\n\nexport function wrapUnit(fraction: number): number {\n\treturn fraction - Math.floor(fraction);\n}\n\nexport function linearTaper(min: number, max: number): Taper {\n\tconst range = max - min;\n\n\treturn {\n\t\tposition: (value) => (range > 0 ? clampUnit((value - min) / range) : 0),\n\t\tvalue: (position) => min + clampUnit(position) * range,\n\t};\n}\n\nexport function logTaper(min: number, max: number): Taper | undefined {\n\tif (min <= 0 || max <= min) return undefined;\n\n\tconst span = Math.log(max / min);\n\n\treturn {\n\t\tposition: (value) => clampUnit(Math.log(Math.max(min, value) / min) / span),\n\t\tvalue: (position) => min * Math.exp(clampUnit(position) * span),\n\t};\n}\n\n// JUCE's skew: a power curve through `midpoint` at half the travel\nexport function skewTaper(min: number, max: number, midpoint: number): Taper | undefined {\n\tconst range = max - min;\n\tconst proportion = (midpoint - min) / range;\n\n\tif (proportion === 0.5 || !(proportion > 0 && proportion < 1)) return undefined;\n\n\tconst skew = Math.log(0.5) / Math.log(proportion);\n\n\treturn {\n\t\tposition: (value) => clampUnit((value - min) / range) ** skew,\n\t\tvalue: (position) => min + range * clampUnit(position) ** (1 / skew),\n\t};\n}\n"],"mappings":";AAMA,SAAgB,UAAU,UAA0B;CACnD,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,CAAC;AACzC;AAEA,SAAgB,SAAS,UAA0B;CAClD,OAAO,WAAW,KAAK,MAAM,QAAQ;AACtC;AAEA,SAAgB,YAAY,KAAa,KAAoB;CAC5D,MAAM,QAAQ,MAAM;CAEpB,OAAO;EACN,WAAW,UAAW,QAAQ,IAAI,WAAW,QAAQ,OAAO,KAAK,IAAI;EACrE,QAAQ,aAAa,MAAM,UAAU,QAAQ,IAAI;CAClD;AACD;AAEA,SAAgB,SAAS,KAAa,KAAgC;CACrE,IAAI,OAAO,KAAK,OAAO,KAAK,OAAO;CAEnC,MAAM,OAAO,KAAK,IAAI,MAAM,GAAG;CAE/B,OAAO;EACN,WAAW,UAAU,UAAU,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,IAAI,IAAI;EAC1E,QAAQ,aAAa,MAAM,KAAK,IAAI,UAAU,QAAQ,IAAI,IAAI;CAC/D;AACD;AAGA,SAAgB,UAAU,KAAa,KAAa,UAAqC;CACxF,MAAM,QAAQ,MAAM;CACpB,MAAM,cAAc,WAAW,OAAO;CAEtC,IAAI,eAAe,MAAO,EAAE,aAAa,KAAK,aAAa,IAAI,OAAO;CAEtE,MAAM,OAAO,KAAK,IAAI,EAAG,IAAI,KAAK,IAAI,UAAU;CAEhD,OAAO;EACN,WAAW,UAAU,WAAW,QAAQ,OAAO,KAAK,KAAK;EACzD,QAAQ,aAAa,MAAM,QAAQ,UAAU,QAAQ,MAAM,IAAI;CAChE;AACD"}
1
+ {"version":3,"file":"taper.js","names":[],"sources":["../../../src/lib/taper.ts"],"sourcesContent":["import { clampUnit } from '#lib/math.ts';\n\n// Positions run 0 to 1 along the travel\nexport interface Taper {\n\tposition: (value: number) => number;\n\tvalue: (position: number) => number;\n}\n\nexport function linearTaper(min: number, max: number): Taper {\n\tconst range = max - min;\n\n\treturn {\n\t\tposition: (value) => (range > 0 ? clampUnit((value - min) / range) : 0),\n\t\tvalue: (position) => min + clampUnit(position) * range,\n\t};\n}\n\nexport function logTaper(min: number, max: number): Taper | undefined {\n\tif (min <= 0 || max <= min) return undefined;\n\n\tconst span = Math.log(max / min);\n\n\treturn {\n\t\tposition: (value) => clampUnit(Math.log(Math.max(min, value) / min) / span),\n\t\tvalue: (position) => min * Math.exp(clampUnit(position) * span),\n\t};\n}\n\n// JUCE's skew: a power curve through `midpoint` at half the travel\nexport function skewTaper(min: number, max: number, midpoint: number): Taper | undefined {\n\tconst range = max - min;\n\tconst proportion = (midpoint - min) / range;\n\n\tif (proportion === 0.5 || !(proportion > 0 && proportion < 1)) return undefined;\n\n\tconst skew = Math.log(0.5) / Math.log(proportion);\n\n\treturn {\n\t\tposition: (value) => clampUnit((value - min) / range) ** skew,\n\t\tvalue: (position) => min + range * clampUnit(position) ** (1 / skew),\n\t};\n}\n\n// Entries sit evenly along the travel, however unevenly they are spaced\nexport function listTaper(entries: Array<number>): Taper {\n\tconst last = entries.length - 1;\n\n\treturn {\n\t\tposition: (value) => {\n\t\t\tconst above = entries.findIndex((entry) => entry >= value);\n\t\t\tif (above === -1) return 1;\n\n\t\t\tconst high = entries[above];\n\t\t\tconst low = entries[above - 1];\n\t\t\tif (high === undefined || low === undefined) return 0;\n\n\t\t\treturn (above - 1 + (value - low) / (high - low)) / last;\n\t\t},\n\t\tvalue: (position) => entries[Math.round(clampUnit(position) * last)] ?? NaN,\n\t};\n}\n"],"mappings":";;;AAQA,SAAgB,YAAY,KAAa,KAAoB;CAC5D,MAAM,QAAQ,MAAM;CAEpB,OAAO;EACN,WAAW,UAAW,QAAQ,IAAI,WAAW,QAAQ,OAAO,KAAK,IAAI;EACrE,QAAQ,aAAa,MAAM,UAAU,QAAQ,IAAI;CAClD;AACD;AAEA,SAAgB,SAAS,KAAa,KAAgC;CACrE,IAAI,OAAO,KAAK,OAAO,KAAK,OAAO;CAEnC,MAAM,OAAO,KAAK,IAAI,MAAM,GAAG;CAE/B,OAAO;EACN,WAAW,UAAU,UAAU,KAAK,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,IAAI,IAAI;EAC1E,QAAQ,aAAa,MAAM,KAAK,IAAI,UAAU,QAAQ,IAAI,IAAI;CAC/D;AACD;AAGA,SAAgB,UAAU,KAAa,KAAa,UAAqC;CACxF,MAAM,QAAQ,MAAM;CACpB,MAAM,cAAc,WAAW,OAAO;CAEtC,IAAI,eAAe,MAAO,EAAE,aAAa,KAAK,aAAa,IAAI,OAAO;CAEtE,MAAM,OAAO,KAAK,IAAI,EAAG,IAAI,KAAK,IAAI,UAAU;CAEhD,OAAO;EACN,WAAW,UAAU,WAAW,QAAQ,OAAO,KAAK,KAAK;EACzD,QAAQ,aAAa,MAAM,QAAQ,UAAU,QAAQ,MAAM,IAAI;CAChE;AACD;AAGA,SAAgB,UAAU,SAA+B;CACxD,MAAM,OAAO,QAAQ,SAAS;CAE9B,OAAO;EACN,WAAW,UAAU;GACpB,MAAM,QAAQ,QAAQ,WAAW,UAAU,SAAS,KAAK;GACzD,IAAI,UAAU,IAAI,OAAO;GAEzB,MAAM,OAAO,QAAQ;GACrB,MAAM,MAAM,QAAQ,QAAQ;GAC5B,IAAI,SAAS,UAAa,QAAQ,QAAW,OAAO;GAEpD,QAAQ,QAAQ,KAAK,QAAQ,QAAQ,OAAO,QAAQ;EACrD;EACA,QAAQ,aAAa,QAAQ,KAAK,MAAM,UAAU,QAAQ,IAAI,IAAI,MAAM;CACzE;AACD"}
@@ -27,9 +27,11 @@
27
27
 
28
28
  sonic-dial:not(:defined, [hidden]) {
29
29
  display: block;
30
+ visibility: hidden;
30
31
  flex-shrink: 0;
31
32
  inline-size: var(--_sonic-dial-size);
32
33
  block-size: var(--_sonic-dial-size);
34
+ overflow: hidden;
33
35
  }
34
36
 
35
37
  .sonic-dial {
@@ -43,6 +45,8 @@
43
45
  /* A fraction of the knob's angle */
44
46
  --_sonic-dial-face-drift: var(--sonic-dial-face-drift, 0.1);
45
47
  --_sonic-dial-notch-ratio: var(--sonic-dial-notch-ratio, 0.1);
48
+ --_sonic-dial-scale-ratio: var(--sonic-dial-scale-ratio, 0.2);
49
+ --_sonic-dial-scale-font-ratio: var(--sonic-dial-scale-font-ratio, 0.175);
46
50
 
47
51
  /* Centred on six o'clock */
48
52
  --_sonic-dial-sweep: var(--sonic-dial-sweep, 300deg);
@@ -52,12 +56,13 @@
52
56
  --_sonic-unit: calc(var(--_sonic-dial-size) / 40);
53
57
  --_sonic-dial-lit: var(--_sonic-lit);
54
58
  --_sonic-dial-notch-band: 0px;
59
+ --_sonic-dial-scale-band: 0px;
55
60
  --_sonic-dial-notch-ends: 1;
56
61
  --_sonic-dial-start: calc(180deg + (1turn - var(--_sonic-dial-sweep)) / 2);
57
62
  --_sonic-dial-angle: calc(var(--_sonic-dial-value, 0) * var(--_sonic-dial-sweep));
58
63
  --_sonic-dial-ring-width: calc(var(--_sonic-dial-size) * var(--_sonic-dial-ring-ratio));
59
64
  --_sonic-dial-cap-inset: calc(
60
- var(--_sonic-dial-notch-band) + var(--_sonic-dial-size) *
65
+ var(--_sonic-dial-scale-band) + var(--_sonic-dial-notch-band) + var(--_sonic-dial-size) *
61
66
  (var(--_sonic-dial-ring-ratio) + var(--_sonic-dial-gap-ratio))
62
67
  );
63
68
 
@@ -99,6 +104,10 @@
99
104
  --_sonic-dial-notch-band: calc(var(--_sonic-dial-size) * var(--_sonic-dial-notch-ratio));
100
105
  }
101
106
 
107
+ :has(> [data-sonic-value]) > .sonic-dial {
108
+ --_sonic-dial-scale-band: calc(var(--_sonic-dial-size) * var(--_sonic-dial-scale-ratio));
109
+ }
110
+
102
111
  /* Set on `--_sonic-dial-lit`, since `--_sonic-lit` cannot reference itself */
103
112
  [dimmed] > .sonic-dial {
104
113
  --_sonic-dial-lit: var(--_sonic-lit-off);
@@ -117,7 +126,7 @@
117
126
  );
118
127
 
119
128
  position: absolute;
120
- inset: 0;
129
+ inset: var(--_sonic-dial-scale-band);
121
130
  background: repeating-conic-gradient(
122
131
  from calc(var(--_sonic-dial-start) - var(--_sonic-dial-notch-width) / 2),
123
132
  var(--_sonic-etch) 0 var(--_sonic-dial-notch-width),
@@ -139,9 +148,40 @@
139
148
  mask-composite: intersect;
140
149
  }
141
150
 
151
+ .sonic-dial-scale {
152
+ font-size: calc(var(--_sonic-dial-size) * var(--_sonic-dial-scale-font-ratio));
153
+
154
+ /* The near edge hugs the band's inner edge, and each mark grows outward along its angle */
155
+ & > * {
156
+ --_sonic-dial-scale-angle: calc(
157
+ var(--_sonic-dial-start) + var(--_sonic-scale-at, 0) * var(--_sonic-dial-sweep)
158
+ );
159
+ --_sonic-dial-scale-sin: sin(var(--_sonic-dial-scale-angle));
160
+ --_sonic-dial-scale-cos: cos(var(--_sonic-dial-scale-angle));
161
+ --_sonic-dial-scale-radius: calc(
162
+ var(--_sonic-dial-size) / 2 - var(--_sonic-dial-scale-band) + var(--_sonic-unit)
163
+ );
164
+
165
+ inset: calc(50% - var(--_sonic-dial-scale-cos) * var(--_sonic-dial-scale-radius)) auto auto
166
+ calc(50% + var(--_sonic-dial-scale-sin) * var(--_sonic-dial-scale-radius));
167
+ translate: calc(-50% + var(--_sonic-dial-scale-sin) * 50%)
168
+ calc(-50% - var(--_sonic-dial-scale-cos) * 50%);
169
+ }
170
+
171
+ & > .sonic-scale-tick {
172
+ --_sonic-dial-tick-length: calc(var(--_sonic-dial-scale-band) * 0.4);
173
+
174
+ inline-size: var(--_sonic-unit);
175
+ block-size: var(--_sonic-dial-tick-length);
176
+ rotate: var(--_sonic-dial-scale-angle);
177
+ translate: calc(-50% + var(--_sonic-dial-scale-sin) * var(--_sonic-dial-tick-length) / 2)
178
+ calc(-50% - var(--_sonic-dial-scale-cos) * var(--_sonic-dial-tick-length) / 2);
179
+ }
180
+ }
181
+
142
182
  :is(.sonic-dial-ring, .sonic-dial-modulation) {
143
183
  position: absolute;
144
- inset: var(--_sonic-dial-notch-band);
184
+ inset: calc(var(--_sonic-dial-scale-band) + var(--_sonic-dial-notch-band));
145
185
  border-radius: 50%;
146
186
  mask: radial-gradient(
147
187
  closest-side,
@@ -4,6 +4,18 @@
4
4
  initial-value: 32px;
5
5
  }
6
6
 
7
+ @property --_sonic-key-busy-angle {
8
+ syntax: '<angle>';
9
+ inherits: false;
10
+ initial-value: 0deg;
11
+ }
12
+
13
+ @keyframes sonic-key-busy {
14
+ to {
15
+ --_sonic-key-busy-angle: 1turn;
16
+ }
17
+ }
18
+
7
19
  @layer sonic {
8
20
  :is(sonic-key, .sonic-key) {
9
21
  --_sonic-key-size: var(--sonic-key-size, 2rem);
@@ -50,11 +62,7 @@
50
62
  font: inherit;
51
63
  color: var(--_sonic-ink);
52
64
  cursor: pointer;
53
- background: linear-gradient(
54
- var(--_sonic-light-fall),
55
- color-mix(in oklab, var(--_sonic-unlit) 55%, #000),
56
- color-mix(in oklab, var(--_sonic-unlit) 85%, #000)
57
- );
65
+ background: var(--_sonic-well-floor);
58
66
  border: 0;
59
67
  border-radius: var(--_sonic-key-corner);
60
68
  box-shadow: var(--_sonic-well);
@@ -66,12 +74,11 @@
66
74
  touch-action: none;
67
75
  }
68
76
 
69
- .sonic-key:active:where(:not(:disabled)),
77
+ .sonic-key:active:where(:not(:disabled, [aria-disabled='true'])),
70
78
  .sonic-key:is([aria-pressed='true'], :where([momentary]:state(pressed)) > *) {
71
79
  --_sonic-key-shift: calc(var(--_sonic-key-depth) / 2);
72
80
  --_sonic-key-scale: var(--_sonic-key-press-scale);
73
- --_sonic-key-drop: calc(0.5 * var(--_sonic-cast-x)) calc(0.5 * var(--_sonic-cast-y))
74
- var(--_sonic-unit) var(--_sonic-drop-shade);
81
+ --_sonic-key-drop: var(--_sonic-press-drop);
75
82
  }
76
83
 
77
84
  .sonic-key:is([aria-pressed='true'], :where([momentary]:state(pressed)) > *) {
@@ -87,26 +94,63 @@
87
94
  );
88
95
  }
89
96
 
90
- .sonic-key:disabled {
97
+ .sonic-key:is(:disabled, [aria-disabled='true']) {
91
98
  --_sonic-lit: var(--_sonic-lit-disabled);
92
99
 
93
100
  color: var(--_sonic-etch);
94
101
  cursor: default;
95
102
  }
96
103
 
97
- .sonic-key:not([aria-pressed='true'], :disabled, :where([momentary]:state(pressed)) > *):is(
98
- :focus-visible,
99
- :active
100
- ) {
104
+ .sonic-key:not(
105
+ [aria-pressed='true'],
106
+ :disabled,
107
+ [aria-disabled='true'],
108
+ :where([momentary]:state(pressed)) > *
109
+ ):is(:focus-visible, :active) {
101
110
  color: var(--_sonic-ink-lift);
102
111
  }
103
112
 
104
113
  @media (hover: hover) {
105
- .sonic-key:not([aria-pressed='true'], :disabled, :where([momentary]:state(pressed)) > *):hover {
114
+ /* fallow-ignore-next-line code-duplication -- `:hover` sits under `(hover: hover)` and its `:focus-visible` partner outside it */
115
+ .sonic-key:not(
116
+ [aria-pressed='true'],
117
+ :disabled,
118
+ [aria-disabled='true'],
119
+ :where([momentary]:state(pressed)) > *
120
+ ):hover {
106
121
  color: var(--_sonic-ink-lift);
107
122
  }
108
123
  }
109
124
 
125
+ /* Turns with the arc, so evenly shaded */
126
+ [busy] > .sonic-key::after {
127
+ position: absolute;
128
+ inset: 0;
129
+ box-sizing: border-box;
130
+ padding: calc(var(--_sonic-key-size) * var(--_sonic-key-bevel-ratio));
131
+ pointer-events: none;
132
+ content: '';
133
+ background: conic-gradient(
134
+ from var(--_sonic-key-busy-angle),
135
+ transparent 0 55%,
136
+ var(--_sonic-lit)
137
+ );
138
+ border-radius: var(--_sonic-key-corner);
139
+ mask:
140
+ linear-gradient(#000 0 0) content-box exclude,
141
+ linear-gradient(#000 0 0);
142
+ animation: sonic-key-busy 1s linear infinite;
143
+
144
+ @media (prefers-reduced-motion: reduce) {
145
+ background: color-mix(in oklab, var(--_sonic-lit) 45%, transparent);
146
+ animation: none;
147
+ }
148
+
149
+ @media (forced-colors: active) {
150
+ forced-color-adjust: none;
151
+ }
152
+ }
153
+
110
154
  .sonic-key-cap {
111
155
  position: absolute;
112
156
  inset: var(--_sonic-key-gap);
@@ -165,6 +209,7 @@
165
209
  drop-shadow(0 0 var(--_sonic-unit) color-mix(in oklab, var(--_sonic-lit) 35%, transparent));
166
210
  }
167
211
 
212
+ /* fallow-ignore-next-line code-duplication -- one pressed state, restated for each part of the cap it reaches */
168
213
  .sonic-key:is([aria-pressed='true'], :where([momentary]:state(pressed)) > *)
169
214
  .sonic-key-cap
170
215
  > [data-sonic-when='off'],
@@ -185,7 +230,7 @@
185
230
  --_sonic-face: var(--_sonic-lit);
186
231
  --_sonic-ink: HighlightText;
187
232
 
188
- &:disabled {
233
+ &:is(:disabled, [aria-disabled='true']) {
189
234
  --_sonic-ink: ButtonFace;
190
235
  }
191
236
 
@@ -8,6 +8,7 @@
8
8
  .sonic-led {
9
9
  --_sonic-led-size: var(--sonic-led-size, 0.5rem);
10
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));
11
12
  --_sonic-unit: calc(var(--_sonic-led-size) / 8);
12
13
  --_sonic-led-bezel: color-mix(in oklab, var(--_sonic-unlit) 55%, #000);
13
14
 
@@ -28,6 +29,8 @@
28
29
  }
29
30
 
30
31
  @media (forced-colors: active) {
32
+ --_sonic-lit-alt: Mark;
33
+
31
34
  forced-color-adjust: none;
32
35
  background: none;
33
36
 
@@ -38,6 +41,10 @@
38
41
  }
39
42
  }
40
43
 
44
+ .sonic-led[data-sonic-lit='alt'] {
45
+ --_sonic-lit: var(--_sonic-lit-alt);
46
+ }
47
+
41
48
  .sonic-led:is(
42
49
  [data-sonic-lit],
43
50
  :where(
@@ -68,7 +75,7 @@
68
75
  background: none;
69
76
 
70
77
  &::after {
71
- background: Highlight;
78
+ background: var(--_sonic-lit);
72
79
  }
73
80
  }
74
81
  }
@@ -4,6 +4,12 @@
4
4
  initial-value: 24px;
5
5
  }
6
6
 
7
+ @property --_sonic-detent-zone {
8
+ syntax: '<length>';
9
+ inherits: true;
10
+ initial-value: 8px;
11
+ }
12
+
7
13
  @layer sonic {
8
14
  /* Resolved per control rather than on `:root`, so each follows its own cap and unit */
9
15
  :is(
@@ -93,6 +99,11 @@
93
99
  color-mix(in oklab, var(--_sonic-focus) 60%, transparent);
94
100
  }
95
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
+
96
107
  :is(.sonic-dial, .sonic-slider) {
97
108
  /* On the control, since the readout's font size reads it */
98
109
  --_sonic-readout-size: var(--sonic-readout-size, 1.5rem);
@@ -153,6 +164,11 @@
153
164
  }
154
165
 
155
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
+ );
156
172
  --_sonic-engraving: drop-shadow(
157
173
  calc(-0.4 * var(--_sonic-cast-x)) calc(-0.4 * var(--_sonic-cast-y))
158
174
  calc(0.4 * var(--_sonic-unit)) rgb(0 0 0 / 30%)
@@ -162,6 +178,12 @@
162
178
  );
163
179
  }
164
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
+
165
187
  /* The well's full blur would fill a slot this narrow */
166
188
  :is(.sonic-meter, .sonic-slider) {
167
189
  --_sonic-groove:
@@ -292,7 +314,34 @@
292
314
  }
293
315
  }
294
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
+
295
340
  @media (forced-colors: active) {
341
+ .sonic-scale-tick {
342
+ forced-color-adjust: none;
343
+ }
344
+
296
345
  :is(
297
346
  .sonic-dial,
298
347
  .sonic-key,
@@ -327,7 +376,7 @@
327
376
 
328
377
  :is(
329
378
  :disabled > :is(.sonic-dial, .sonic-slider),
330
- .sonic-key:disabled,
379
+ .sonic-key:is(:disabled, [aria-disabled='true']),
331
380
  .sonic-segmented-segment:disabled
332
381
  ) {
333
382
  --_sonic-ink: GrayText;