@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,70 @@
1
+ import { requireChild, template } from "../lib/render.js";
2
+ import { SonicRangeElement } from "./range-element.js";
3
+
4
+ //#region src/elements/slider.ts
5
+ const renderSlider = template(`
6
+ <div class="sonic-slider" role="slider" tabindex="0">
7
+ <div class="sonic-slider-marks"></div>
8
+ <div class="sonic-slider-groove"></div>
9
+ <div class="sonic-slider-cap"></div>
10
+ <div class="sonic-slider-readout" popover="manual">
11
+ <span></span>
12
+ <input
13
+ autocomplete="off"
14
+ class="sonic-slider-entry"
15
+ enterkeyhint="done"
16
+ hidden
17
+ spellcheck="false"
18
+ type="text"
19
+ />
20
+ </div>
21
+ </div>
22
+ `, HTMLDivElement);
23
+ var SonicSlider = class extends SonicRangeElement {
24
+ static observedAttributes = [...SonicRangeElement.observedAttributes, "orientation"];
25
+ #slider = renderSlider();
26
+ connect(signal) {
27
+ const slider = this.#slider;
28
+ const cap = requireChild(slider, ".sonic-slider-cap", HTMLDivElement);
29
+ this.appendOnce(slider);
30
+ this.render();
31
+ this.checkStyles(slider, "slider.css");
32
+ this.bindGestures(slider, signal, (event) => {
33
+ const axis = this.#axis(slider, cap);
34
+ if (event.target instanceof Node && cap.contains(event.target)) return axis;
35
+ if (this.getAttribute("groove-press") === "none") return;
36
+ this.input(this.valueAt((axis.position(event) - axis.startPx) / axis.travelPx));
37
+ return axis;
38
+ });
39
+ }
40
+ render() {
41
+ const slider = this.#slider;
42
+ const positions = this.positions();
43
+ slider.style.setProperty("--_sonic-slider-value", String(this.fraction(this.value)));
44
+ slider.style.setProperty("--_sonic-slider-origin", String(this.originFraction()));
45
+ if (positions === void 0) slider.style.removeProperty("--_sonic-slider-positions");
46
+ else slider.style.setProperty("--_sonic-slider-positions", String(positions));
47
+ this.renderAria(slider, this.#isVertical() ? "vertical" : "horizontal");
48
+ }
49
+ #axis(slider, cap) {
50
+ const track = slider.getBoundingClientRect();
51
+ const capBox = cap.getBoundingClientRect();
52
+ if (this.#isVertical()) return {
53
+ position: (event) => -event.clientY,
54
+ startPx: capBox.height / 2 - track.bottom,
55
+ travelPx: Math.max(1, track.height - capBox.height)
56
+ };
57
+ return {
58
+ position: (event) => event.clientX,
59
+ startPx: track.left + capBox.width / 2,
60
+ travelPx: Math.max(1, track.width - capBox.width)
61
+ };
62
+ }
63
+ #isVertical() {
64
+ return this.getAttribute("orientation") === "vertical";
65
+ }
66
+ };
67
+
68
+ //#endregion
69
+ export { SonicSlider };
70
+ //# sourceMappingURL=slider.js.map
@@ -0,0 +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-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'orientation',\n\t];\n\n\treadonly #slider = renderSlider();\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.getAttribute('groove-press') === '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 render(): void {\n\t\tconst slider = this.#slider;\n\t\tconst positions = this.positions();\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\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.#isVertical() ? 'vertical' : 'horizontal');\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.#isVertical()) {\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#isVertical(): boolean {\n\t\treturn this.getAttribute('orientation') === 'vertical';\n\t}\n}\n"],"mappings":";;;;AAgBA,MAAM,eAAe,SACT;;;;;;;;;;;;;;;;;IAkBX,cACD;AAEA,IAAa,cAAb,cAAiC,kBAAkB;CAClD,OAAyB,qBAAqB,CAC7C,GAAG,kBAAkB,oBACrB,aACD;CAEA,AAAS,UAAU,aAAa;CAEhC,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,aAAa,cAAc,MAAM,QAAQ;GAElD,KAAK,MAAM,KAAK,SAAS,KAAK,SAAS,KAAK,IAAI,KAAK,WAAW,KAAK,QAAQ,CAAC;GAE9E,OAAO;EACR,CAAC;CACF;CAEA,AAAU,SAAe;EACxB,MAAM,SAAS,KAAK;EACpB,MAAM,YAAY,KAAK,UAAU;EAEjC,OAAO,MAAM,YAAY,yBAAyB,OAAO,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC;EACnF,OAAO,MAAM,YAAY,0BAA0B,OAAO,KAAK,eAAe,CAAC,CAAC;EAChF,IAAI,cAAc,QAAW,OAAO,MAAM,eAAe,2BAA2B;OAC/E,OAAO,MAAM,YAAY,6BAA6B,OAAO,SAAS,CAAC;EAC5E,KAAK,WAAW,QAAQ,KAAK,YAAY,IAAI,aAAa,YAAY;CACvE;CAEA,MAAM,QAAqB,KAA8B;EACxD,MAAM,QAAQ,OAAO,sBAAsB;EAC3C,MAAM,SAAS,IAAI,sBAAsB;EAEzC,IAAI,KAAK,YAAY,GACpB,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;CAEA,cAAuB;EACtB,OAAO,KAAK,aAAa,aAAa,MAAM;CAC7C;AACD"}
@@ -0,0 +1,20 @@
1
+ //#region src/elements/sonic-element.d.ts
2
+ export declare abstract class SonicElement extends HTMLElement {
3
+ #private;
4
+ static readonly observedAttributes: string[];
5
+ get disabled(): boolean;
6
+ set disabled(isDisabled: boolean);
7
+ connectedCallback(): void;
8
+ connectedMoveCallback(): void;
9
+ disconnectedCallback(): void;
10
+ protected adoptChildren(adopt: () => void, signal: AbortSignal): void;
11
+ protected appendOnce(child: Element): void;
12
+ protected checkStyles(control: HTMLElement, sheet: string): void;
13
+ protected abstract connect(signal: AbortSignal): void;
14
+ protected forwardNaming(target: Element, isNamed: boolean): void;
15
+ protected numberAttribute(name: string, fallback: number): number;
16
+ protected toggleState(state: string, isOn: boolean): void;
17
+ protected upgradeProperty(name: keyof this): void;
18
+ }
19
+ //#endregion
20
+ //# sourceMappingURL=sonic-element.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { writeAttribute } from "../lib/write-attribute.js";
2
+
3
+ //#region src/elements/sonic-element.ts
4
+ const namingAttributes = [
5
+ "aria-describedby",
6
+ "aria-label",
7
+ "aria-labelledby"
8
+ ];
9
+ const checkedClasses = /* @__PURE__ */ new WeakSet();
10
+ var SonicElement = class extends HTMLElement {
11
+ static observedAttributes = [...namingAttributes, "disabled"];
12
+ get disabled() {
13
+ return this.hasAttribute("disabled");
14
+ }
15
+ set disabled(isDisabled) {
16
+ this.toggleAttribute("disabled", isDisabled);
17
+ }
18
+ #connection;
19
+ #internals = "attachInternals" in HTMLElement.prototype ? this.attachInternals() : void 0;
20
+ connectedCallback() {
21
+ this.upgradeProperty("disabled");
22
+ if (this.#connection) return;
23
+ const connection = new AbortController();
24
+ try {
25
+ this.connect(connection.signal);
26
+ } catch (error) {
27
+ connection.abort();
28
+ throw error;
29
+ }
30
+ this.#connection = connection;
31
+ }
32
+ connectedMoveCallback() {}
33
+ disconnectedCallback() {
34
+ const connection = this.#connection;
35
+ if (!connection) return;
36
+ queueMicrotask(() => {
37
+ if (this.isConnected || this.#connection !== connection) return;
38
+ connection.abort();
39
+ this.#connection = void 0;
40
+ });
41
+ }
42
+ adoptChildren(adopt, signal) {
43
+ const observer = new MutationObserver(adopt);
44
+ adopt();
45
+ observer.observe(this, { childList: true });
46
+ signal.addEventListener("abort", () => {
47
+ observer.disconnect();
48
+ }, { once: true });
49
+ }
50
+ appendOnce(child) {
51
+ if (child.parentNode !== this) this.append(child);
52
+ }
53
+ checkStyles(control, sheet) {
54
+ const elementClass = this.constructor;
55
+ if (checkedClasses.has(elementClass)) return;
56
+ checkedClasses.add(elementClass);
57
+ requestAnimationFrame(() => {
58
+ if (!control.isConnected) {
59
+ checkedClasses.delete(elementClass);
60
+ return;
61
+ }
62
+ const styles = getComputedStyle(control);
63
+ const missing = [styles.getPropertyValue("--_sonic-unlit") === "" ? "material.css" : "", styles.getPropertyValue("--_sonic-unit") === "" ? sheet : ""].filter(Boolean);
64
+ if (missing.length === 0) return;
65
+ console.warn(`<${this.localName}> draws blank without ${missing.map((file) => `@xsynaptic/sonic-ui/${file}`).join(" and ")} (or controls.css)`);
66
+ });
67
+ }
68
+ forwardNaming(target, isNamed) {
69
+ for (const name of namingAttributes) {
70
+ const value = this.getAttribute(name);
71
+ writeAttribute(target, name, isNamed && value !== null ? value : void 0);
72
+ }
73
+ }
74
+ numberAttribute(name, fallback) {
75
+ const attribute = this.getAttribute(name);
76
+ if (attribute === null) return fallback;
77
+ const parsed = Number(attribute);
78
+ return Number.isFinite(parsed) ? parsed : fallback;
79
+ }
80
+ toggleState(state, isOn) {
81
+ if (isOn) this.#internals?.states.add(state);
82
+ else this.#internals?.states.delete(state);
83
+ }
84
+ upgradeProperty(name) {
85
+ if (!Object.hasOwn(this, name)) return;
86
+ const value = this[name];
87
+ Reflect.deleteProperty(this, name);
88
+ this[name] = value;
89
+ }
90
+ };
91
+
92
+ //#endregion
93
+ export { SonicElement };
94
+ //# sourceMappingURL=sonic-element.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sonic-element.js","names":[],"sources":["../../src/elements/sonic-element.ts"],"sourcesContent":["import { writeAttribute } from '#lib/write-attribute.ts';\n\n// Forwarding another attribute is a public API decision\nconst namingAttributes = ['aria-describedby', 'aria-label', 'aria-labelledby'];\n\nconst checkedClasses = new WeakSet<object>();\n\n// eslint-disable-next-line wc/define-tag-after-class-definition -- abstract, so only its subclasses are ever defined\nexport abstract class SonicElement extends HTMLElement {\n\tstatic readonly observedAttributes = [...namingAttributes, 'disabled'];\n\n\tget disabled(): boolean {\n\t\treturn this.hasAttribute('disabled');\n\t}\n\n\tset disabled(isDisabled: boolean) {\n\t\tthis.toggleAttribute('disabled', isDisabled);\n\t}\n\n\t#connection: AbortController | undefined;\n\n\t// happy-dom has no `ElementInternals`\n\treadonly #internals =\n\t\t'attachInternals' in HTMLElement.prototype ? this.attachInternals() : undefined;\n\n\tconnectedCallback(): void {\n\t\tthis.upgradeProperty('disabled');\n\t\tif (this.#connection) return;\n\n\t\tconst connection = new AbortController();\n\n\t\t// A throw leaves nothing bound behind it\n\t\ttry {\n\t\t\tthis.connect(connection.signal);\n\t\t} catch (error) {\n\t\t\tconnection.abort();\n\t\t\tthrow error;\n\t\t}\n\n\t\tthis.#connection = connection;\n\t}\n\n\tconnectedMoveCallback(): void {\n\t\t// Defined so `moveBefore` from a persisting router keeps every binding\n\t}\n\n\t// Without `moveBefore` (Safari), a persisting router disconnects and reconnects in one task\n\tdisconnectedCallback(): void {\n\t\tconst connection = this.#connection;\n\t\tif (!connection) return;\n\n\t\tqueueMicrotask(() => {\n\t\t\tif (this.isConnected || this.#connection !== connection) return;\n\n\t\t\tconnection.abort();\n\t\t\tthis.#connection = undefined;\n\t\t});\n\t}\n\n\t// Replacing the host's children takes the control with them, so `adopt` runs on every change\n\tprotected adoptChildren(adopt: () => void, signal: AbortSignal): void {\n\t\tconst observer = new MutationObserver(adopt);\n\n\t\tadopt();\n\t\tobserver.observe(this, { childList: true });\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tobserver.disconnect();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t}\n\n\tprotected appendOnce(child: Element): void {\n\t\tif (child.parentNode !== this) this.append(child);\n\t}\n\n\t// A missing sheet draws the control blank without an error\n\tprotected checkStyles(control: HTMLElement, sheet: string): void {\n\t\tconst elementClass = this.constructor;\n\t\tif (checkedClasses.has(elementClass)) return;\n\n\t\tcheckedClasses.add(elementClass);\n\t\trequestAnimationFrame(() => {\n\t\t\t// A detached control computes no styles\n\t\t\tif (!control.isConnected) {\n\t\t\t\tcheckedClasses.delete(elementClass);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst styles = getComputedStyle(control);\n\t\t\tconst missing = [\n\t\t\t\tstyles.getPropertyValue('--_sonic-unlit') === '' ? 'material.css' : '',\n\t\t\t\tstyles.getPropertyValue('--_sonic-unit') === '' ? sheet : '',\n\t\t\t].filter(Boolean);\n\t\t\tif (missing.length === 0) return;\n\n\t\t\tconsole.warn(\n\t\t\t\t`<${this.localName}> draws blank without ${missing.map((file) => `@xsynaptic/sonic-ui/${file}`).join(' and ')} (or controls.css)`,\n\t\t\t);\n\t\t});\n\t}\n\n\tprotected abstract connect(signal: AbortSignal): void;\n\n\tprotected forwardNaming(target: Element, isNamed: boolean): void {\n\t\tfor (const name of namingAttributes) {\n\t\t\tconst value = this.getAttribute(name);\n\n\t\t\twriteAttribute(target, name, isNamed && value !== null ? value : undefined);\n\t\t}\n\t}\n\n\tprotected numberAttribute(name: string, fallback: number): number {\n\t\tconst attribute = this.getAttribute(name);\n\t\tif (attribute === null) return fallback;\n\n\t\tconst parsed = Number(attribute);\n\n\t\treturn Number.isFinite(parsed) ? parsed : fallback;\n\t}\n\n\tprotected toggleState(state: string, isOn: boolean): void {\n\t\tif (isOn) this.#internals?.states.add(state);\n\t\telse this.#internals?.states.delete(state);\n\t}\n\n\t// A property set before upgrade shadows its accessor\n\tprotected upgradeProperty(name: keyof this): void {\n\t\tif (!Object.hasOwn(this, name)) return;\n\n\t\tconst value = this[name];\n\n\t\tReflect.deleteProperty(this, name);\n\t\tthis[name] = value;\n\t}\n}\n"],"mappings":";;;AAGA,MAAM,mBAAmB;CAAC;CAAoB;CAAc;AAAiB;AAE7E,MAAM,iCAAiB,IAAI,QAAgB;AAG3C,IAAsB,eAAtB,cAA2C,YAAY;CACtD,OAAgB,qBAAqB,CAAC,GAAG,kBAAkB,UAAU;CAErE,IAAI,WAAoB;EACvB,OAAO,KAAK,aAAa,UAAU;CACpC;CAEA,IAAI,SAAS,YAAqB;EACjC,KAAK,gBAAgB,YAAY,UAAU;CAC5C;CAEA;CAGA,AAAS,aACR,qBAAqB,YAAY,YAAY,KAAK,gBAAgB,IAAI;CAEvE,oBAA0B;EACzB,KAAK,gBAAgB,UAAU;EAC/B,IAAI,KAAK,aAAa;EAEtB,MAAM,aAAa,IAAI,gBAAgB;EAGvC,IAAI;GACH,KAAK,QAAQ,WAAW,MAAM;EAC/B,SAAS,OAAO;GACf,WAAW,MAAM;GACjB,MAAM;EACP;EAEA,KAAK,cAAc;CACpB;CAEA,wBAA8B,CAE9B;CAGA,uBAA6B;EAC5B,MAAM,aAAa,KAAK;EACxB,IAAI,CAAC,YAAY;EAEjB,qBAAqB;GACpB,IAAI,KAAK,eAAe,KAAK,gBAAgB,YAAY;GAEzD,WAAW,MAAM;GACjB,KAAK,cAAc;EACpB,CAAC;CACF;CAGA,AAAU,cAAc,OAAmB,QAA2B;EACrE,MAAM,WAAW,IAAI,iBAAiB,KAAK;EAE3C,MAAM;EACN,SAAS,QAAQ,MAAM,EAAE,WAAW,KAAK,CAAC;EAC1C,OAAO,iBACN,eACM;GACL,SAAS,WAAW;EACrB,GACA,EAAE,MAAM,KAAK,CACd;CACD;CAEA,AAAU,WAAW,OAAsB;EAC1C,IAAI,MAAM,eAAe,MAAM,KAAK,OAAO,KAAK;CACjD;CAGA,AAAU,YAAY,SAAsB,OAAqB;EAChE,MAAM,eAAe,KAAK;EAC1B,IAAI,eAAe,IAAI,YAAY,GAAG;EAEtC,eAAe,IAAI,YAAY;EAC/B,4BAA4B;GAE3B,IAAI,CAAC,QAAQ,aAAa;IACzB,eAAe,OAAO,YAAY;IAClC;GACD;GAEA,MAAM,SAAS,iBAAiB,OAAO;GACvC,MAAM,UAAU,CACf,OAAO,iBAAiB,gBAAgB,MAAM,KAAK,iBAAiB,IACpE,OAAO,iBAAiB,eAAe,MAAM,KAAK,QAAQ,EAC3D,CAAC,CAAC,OAAO,OAAO;GAChB,IAAI,QAAQ,WAAW,GAAG;GAE1B,QAAQ,KACP,IAAI,KAAK,UAAU,wBAAwB,QAAQ,KAAK,SAAS,uBAAuB,MAAM,CAAC,CAAC,KAAK,OAAO,EAAE,mBAC/G;EACD,CAAC;CACF;CAIA,AAAU,cAAc,QAAiB,SAAwB;EAChE,KAAK,MAAM,QAAQ,kBAAkB;GACpC,MAAM,QAAQ,KAAK,aAAa,IAAI;GAEpC,eAAe,QAAQ,MAAM,WAAW,UAAU,OAAO,QAAQ,MAAS;EAC3E;CACD;CAEA,AAAU,gBAAgB,MAAc,UAA0B;EACjE,MAAM,YAAY,KAAK,aAAa,IAAI;EACxC,IAAI,cAAc,MAAM,OAAO;EAE/B,MAAM,SAAS,OAAO,SAAS;EAE/B,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS;CAC3C;CAEA,AAAU,YAAY,OAAe,MAAqB;EACzD,IAAI,MAAM,KAAK,YAAY,OAAO,IAAI,KAAK;OACtC,KAAK,YAAY,OAAO,OAAO,KAAK;CAC1C;CAGA,AAAU,gBAAgB,MAAwB;EACjD,IAAI,CAAC,OAAO,OAAO,MAAM,IAAI,GAAG;EAEhC,MAAM,QAAQ,KAAK;EAEnB,QAAQ,eAAe,MAAM,IAAI;EACjC,KAAK,QAAQ;CACd;AACD"}
@@ -0,0 +1,6 @@
1
+ import { SonicDial } from "./elements/dial.js";
2
+ import { SonicKey } from "./elements/key.js";
3
+ import { SonicMeter } from "./elements/meter.js";
4
+ import { SonicSegmented } from "./elements/segmented.js";
5
+ import { SonicSlider } from "./elements/slider.js";
6
+ export { SonicDial, SonicKey, SonicMeter, SonicSegmented, SonicSlider };
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { SonicDial } from "./elements/dial.js";
2
+ import { SonicKey } from "./elements/key.js";
3
+ import { SonicMeter } from "./elements/meter.js";
4
+ import { SonicSegmented } from "./elements/segmented.js";
5
+ import { SonicSlider } from "./elements/slider.js";
6
+
7
+ export { SonicDial, SonicKey, SonicMeter, SonicSegmented, SonicSlider };
@@ -0,0 +1,9 @@
1
+ //#region src/lib/read-px-property.ts
2
+ function readPxProperty(styles, property, fallback) {
3
+ const parsed = Number.parseFloat(styles.getPropertyValue(property));
4
+ return Number.isFinite(parsed) ? parsed : fallback;
5
+ }
6
+
7
+ //#endregion
8
+ export { readPxProperty };
9
+ //# sourceMappingURL=read-px-property.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-px-property.js","names":[],"sources":["../../src/lib/read-px-property.ts"],"sourcesContent":["// Only px; another unit needs a probe element to convert\nexport function readPxProperty(\n\tstyles: CSSStyleDeclaration,\n\tproperty: string,\n\tfallback: number,\n): number {\n\t// eslint-disable-next-line unicorn/prefer-number-coercion -- parsing has to stop at the unit\n\tconst parsed = Number.parseFloat(styles.getPropertyValue(property));\n\n\treturn Number.isFinite(parsed) ? parsed : fallback;\n}\n"],"mappings":";AACA,SAAgB,eACf,QACA,UACA,UACS;CAET,MAAM,SAAS,OAAO,WAAW,OAAO,iBAAiB,QAAQ,CAAC;CAElE,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC3C"}
@@ -0,0 +1,19 @@
1
+ //#region src/lib/render.ts
2
+ function requireChild(root, selector, child) {
3
+ const found = root.querySelector(selector);
4
+ if (!(found instanceof child)) throw new Error(`A template is missing its ${selector}`);
5
+ return found;
6
+ }
7
+ function template(html, root) {
8
+ const parsed = document.createElement("template");
9
+ parsed.innerHTML = html;
10
+ return () => {
11
+ const clone = document.importNode(parsed.content, true).firstElementChild;
12
+ if (!(clone instanceof root)) throw new Error(`A template's root is not a ${root.name}`);
13
+ return clone;
14
+ };
15
+ }
16
+
17
+ //#endregion
18
+ export { requireChild, template };
19
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","names":[],"sources":["../../src/lib/render.ts"],"sourcesContent":["export function requireChild<Child extends Element>(\n\troot: Element,\n\tselector: string,\n\tchild: new () => Child,\n): Child {\n\tconst found = root.querySelector(selector);\n\tif (!(found instanceof child)) throw new Error(`A template is missing its ${selector}`);\n\n\treturn found;\n}\n\nexport function template<Root extends Element>(html: string, root: new () => Root): () => Root {\n\tconst parsed = document.createElement('template');\n\n\tparsed.innerHTML = html;\n\n\treturn () => {\n\t\tconst clone = document.importNode(parsed.content, true).firstElementChild;\n\t\tif (!(clone instanceof root)) throw new Error(`A template's root is not a ${root.name}`);\n\n\t\treturn clone;\n\t};\n}\n"],"mappings":";AAAA,SAAgB,aACf,MACA,UACA,OACQ;CACR,MAAM,QAAQ,KAAK,cAAc,QAAQ;CACzC,IAAI,EAAE,iBAAiB,QAAQ,MAAM,IAAI,MAAM,6BAA6B,UAAU;CAEtF,OAAO;AACR;AAEA,SAAgB,SAA+B,MAAc,MAAkC;CAC9F,MAAM,SAAS,SAAS,cAAc,UAAU;CAEhD,OAAO,YAAY;CAEnB,aAAa;EACZ,MAAM,QAAQ,SAAS,WAAW,OAAO,SAAS,IAAI,CAAC,CAAC;EACxD,IAAI,EAAE,iBAAiB,OAAO,MAAM,IAAI,MAAM,8BAA8B,KAAK,MAAM;EAEvF,OAAO;CACR;AACD"}
@@ -0,0 +1,33 @@
1
+ //#region src/lib/taper.ts
2
+ function clampUnit(fraction) {
3
+ return Math.min(1, Math.max(0, fraction));
4
+ }
5
+ function linearTaper(min, max) {
6
+ const range = max - min;
7
+ return {
8
+ position: (value) => range > 0 ? clampUnit((value - min) / range) : 0,
9
+ value: (position) => min + clampUnit(position) * range
10
+ };
11
+ }
12
+ function logTaper(min, max) {
13
+ if (min <= 0 || max <= min) return void 0;
14
+ const span = Math.log(max / min);
15
+ return {
16
+ position: (value) => clampUnit(Math.log(Math.max(min, value) / min) / span),
17
+ value: (position) => min * Math.exp(clampUnit(position) * span)
18
+ };
19
+ }
20
+ function skewTaper(min, max, midpoint) {
21
+ const range = max - min;
22
+ const proportion = (midpoint - min) / range;
23
+ if (proportion === .5 || !(proportion > 0 && proportion < 1)) return void 0;
24
+ const skew = Math.log(.5) / Math.log(proportion);
25
+ return {
26
+ position: (value) => clampUnit((value - min) / range) ** skew,
27
+ value: (position) => min + range * clampUnit(position) ** (1 / skew)
28
+ };
29
+ }
30
+
31
+ //#endregion
32
+ export { clampUnit, linearTaper, logTaper, skewTaper };
33
+ //# sourceMappingURL=taper.js.map
@@ -0,0 +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 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,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"}
@@ -0,0 +1,12 @@
1
+ //#region src/lib/write-attribute.ts
2
+ function writeAttribute(element, name, value) {
3
+ if (value === void 0) {
4
+ element.removeAttribute(name);
5
+ return;
6
+ }
7
+ element.setAttribute(name, value);
8
+ }
9
+
10
+ //#endregion
11
+ export { writeAttribute };
12
+ //# sourceMappingURL=write-attribute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-attribute.js","names":[],"sources":["../../src/lib/write-attribute.ts"],"sourcesContent":["export function writeAttribute(element: Element, name: string, value: string | undefined): void {\n\tif (value === undefined) {\n\t\telement.removeAttribute(name);\n\t\treturn;\n\t}\n\n\telement.setAttribute(name, value);\n}\n"],"mappings":";AAAA,SAAgB,eAAe,SAAkB,MAAc,OAAiC;CAC/F,IAAI,UAAU,QAAW;EACxB,QAAQ,gBAAgB,IAAI;EAC5B;CACD;CAEA,QAAQ,aAAa,MAAM,KAAK;AACjC"}
@@ -0,0 +1,8 @@
1
+ /* Ink derived from a pale cap would vanish against it */
2
+ @layer sonic {
3
+ .sonic-skin-ivory {
4
+ --sonic-cap: oklch(85.5% 0.0281 215deg);
5
+ --sonic-cap-hover: oklch(90.5% 0.024 215deg);
6
+ --sonic-mark: oklch(18.5% 0.0099 237deg);
7
+ }
8
+ }
@@ -0,0 +1,10 @@
1
+ @layer sonic {
2
+ .sonic-skin-lime {
3
+ --sonic-lit: oklch(83.8% 0.205 118deg);
4
+ --sonic-unlit: oklch(28.6% 0.0203 237deg);
5
+ --sonic-cap: oklch(32.4% 0.0229 237deg);
6
+ --sonic-cap-hover: oklch(37.2% 0.025 237deg);
7
+ --sonic-dial-ring-ratio: 0.05;
8
+ --sonic-dial-gap-ratio: 0.1;
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ @layer sonic {
2
+ .sonic-skin-slate {
3
+ --sonic-lit: #22d3ee;
4
+ --sonic-unlit: #1e293b;
5
+ --sonic-cap: #334155;
6
+ --sonic-cap-hover: #475569;
7
+ --sonic-dial-ring-ratio: 0.12;
8
+ --sonic-dial-gap-ratio: 0.07;
9
+ }
10
+ }
@@ -0,0 +1,8 @@
1
+ /* The material first; every control's sheet reads it */
2
+ @import './material.css';
3
+ @import './dial.css';
4
+ @import './key.css';
5
+ @import './meter.css';
6
+ @import './screen.css';
7
+ @import './segmented.css';
8
+ @import './slider.css';
@@ -0,0 +1,265 @@
1
+ @layer sonic {
2
+ /* On the host too, for the placeholder */
3
+ :is(sonic-dial, .sonic-dial) {
4
+ --_sonic-dial-size: var(--sonic-dial-size, 2.5rem);
5
+ }
6
+
7
+ :has(> .sonic-dial):not([hidden]) {
8
+ display: contents;
9
+ }
10
+
11
+ sonic-dial:not(:defined, [hidden]) {
12
+ display: block;
13
+ flex-shrink: 0;
14
+ inline-size: var(--_sonic-dial-size);
15
+ block-size: var(--_sonic-dial-size);
16
+ }
17
+
18
+ .sonic-dial {
19
+ --_sonic-dial-ring-ratio: var(--sonic-dial-ring-ratio, 0.075);
20
+ --_sonic-dial-gap-ratio: var(--sonic-dial-gap-ratio, 0.05);
21
+ --_sonic-dial-pointer-ratio: var(--sonic-dial-pointer-ratio, 0.06);
22
+
23
+ /* 0 for a straight bar */
24
+ --_sonic-dial-pointer-taper: var(--sonic-dial-pointer-taper, 0.5);
25
+
26
+ /* A fraction of the knob's angle */
27
+ --_sonic-dial-face-drift: var(--sonic-dial-face-drift, 0.1);
28
+ --_sonic-dial-notch-ratio: var(--sonic-dial-notch-ratio, 0.1);
29
+
30
+ /* Centred on six o'clock */
31
+ --_sonic-dial-sweep: var(--sonic-dial-sweep, 300deg);
32
+
33
+ /* Absolute, so a larger knob gets more ridges rather than wider ones */
34
+ --_sonic-dial-ridge-pitch: var(--sonic-dial-ridge-pitch, 4px);
35
+
36
+ /* Read by the element as px */
37
+ --_sonic-dial-travel: var(--sonic-dial-travel, 160px);
38
+ --_sonic-unit: calc(var(--_sonic-dial-size) / 40);
39
+ --_sonic-dial-lit: var(--_sonic-lit);
40
+ --_sonic-dial-notch-band: 0px;
41
+ --_sonic-dial-start: calc(180deg + (1turn - var(--_sonic-dial-sweep)) / 2);
42
+ --_sonic-dial-angle: calc(var(--_sonic-dial-value, 0) * var(--_sonic-dial-sweep));
43
+ --_sonic-dial-ring-width: calc(var(--_sonic-dial-size) * var(--_sonic-dial-ring-ratio));
44
+ --_sonic-dial-cap-inset: calc(
45
+ var(--_sonic-dial-notch-band) + var(--_sonic-dial-size) *
46
+ (var(--_sonic-dial-ring-ratio) + var(--_sonic-dial-gap-ratio))
47
+ );
48
+
49
+ /* `tan(atan2())` divides one length by another, which plain `calc()` can't yet do everywhere */
50
+ --_sonic-dial-ridge-angle: calc(
51
+ 1turn /
52
+ round(
53
+ tan(
54
+ atan2(
55
+ var(--_sonic-dial-size) - 2 * var(--_sonic-dial-cap-inset),
56
+ var(--_sonic-dial-ridge-pitch)
57
+ )
58
+ ) *
59
+ 3.1416,
60
+ 1
61
+ )
62
+ );
63
+
64
+ position: relative;
65
+ flex-shrink: 0;
66
+ inline-size: var(--_sonic-dial-size);
67
+ block-size: var(--_sonic-dial-size);
68
+ touch-action: none;
69
+ cursor: ns-resize;
70
+ user-select: none;
71
+ border-radius: 50%;
72
+ }
73
+
74
+ :state(dragging) > .sonic-dial {
75
+ --_sonic-cap: var(--_sonic-cap-hover);
76
+ }
77
+
78
+ [notched] > .sonic-dial {
79
+ --_sonic-dial-notch-band: calc(var(--_sonic-dial-size) * var(--_sonic-dial-notch-ratio));
80
+ }
81
+
82
+ /* Set on `--_sonic-dial-lit`, since `--_sonic-lit` cannot reference itself */
83
+ [dimmed] > .sonic-dial {
84
+ --_sonic-dial-lit: var(--_sonic-lit-off);
85
+ }
86
+
87
+ [disabled] > .sonic-dial {
88
+ --_sonic-dial-lit: var(--_sonic-lit-disabled);
89
+
90
+ cursor: default;
91
+ }
92
+
93
+ .sonic-dial-notches {
94
+ --_sonic-dial-notch-width: atan2(var(--_sonic-unit), var(--_sonic-dial-size) / 2);
95
+ --_sonic-dial-notch-pitch: calc(
96
+ var(--_sonic-dial-sweep) / (var(--_sonic-dial-positions, 2) - 1)
97
+ );
98
+
99
+ position: absolute;
100
+ inset: 0;
101
+ background: repeating-conic-gradient(
102
+ from calc(var(--_sonic-dial-start) - var(--_sonic-dial-notch-width) / 2),
103
+ var(--_sonic-etch) 0 var(--_sonic-dial-notch-width),
104
+ transparent 0 var(--_sonic-dial-notch-pitch)
105
+ );
106
+ border-radius: 50%;
107
+ mask:
108
+ radial-gradient(
109
+ closest-side,
110
+ transparent calc(100% - var(--_sonic-dial-notch-band) * 0.8),
111
+ #000 0 calc(100% - var(--_sonic-dial-notch-band) * 0.25),
112
+ transparent 0
113
+ ),
114
+ conic-gradient(
115
+ from calc(var(--_sonic-dial-start) - var(--_sonic-dial-notch-width)),
116
+ #000 calc(var(--_sonic-dial-sweep) + 2 * var(--_sonic-dial-notch-width)),
117
+ transparent 0
118
+ );
119
+ mask-composite: intersect;
120
+ }
121
+
122
+ :is(.sonic-dial-ring, .sonic-dial-modulation) {
123
+ position: absolute;
124
+ inset: var(--_sonic-dial-notch-band);
125
+ border-radius: 50%;
126
+ mask: radial-gradient(
127
+ closest-side,
128
+ transparent calc(100% - var(--_sonic-dial-ring-width) - 0.5px),
129
+ #000 calc(100% - var(--_sonic-dial-ring-width)),
130
+ #000 calc(100% - var(--_sonic-dial-band-inset) - 0.5px),
131
+ transparent calc(100% - var(--_sonic-dial-band-inset))
132
+ );
133
+ }
134
+
135
+ .sonic-dial-ring {
136
+ --_sonic-dial-band-inset: 0px;
137
+ --_sonic-dial-lit-from: calc(
138
+ min(var(--_sonic-dial-origin, 0), var(--_sonic-dial-value, 0)) * var(--_sonic-dial-sweep)
139
+ );
140
+ --_sonic-dial-lit-to: calc(
141
+ max(var(--_sonic-dial-origin, 0), var(--_sonic-dial-value, 0)) * var(--_sonic-dial-sweep)
142
+ );
143
+
144
+ background:
145
+ radial-gradient(
146
+ closest-side,
147
+ rgb(0 0 0 / 55%) calc(100% - var(--_sonic-dial-ring-width)),
148
+ transparent calc(100% - var(--_sonic-dial-ring-width) * 0.4),
149
+ rgb(0 0 0 / 25%) 100%
150
+ ),
151
+ conic-gradient(
152
+ from var(--_sonic-dial-start),
153
+ var(--_sonic-unlit) var(--_sonic-dial-lit-from),
154
+ var(--_sonic-dial-lit) 0 var(--_sonic-dial-lit-to),
155
+ var(--_sonic-unlit) 0 var(--_sonic-dial-sweep),
156
+ transparent 0
157
+ );
158
+ }
159
+
160
+ .sonic-dial-modulation {
161
+ --_sonic-dial-modulation: var(
162
+ --sonic-modulation,
163
+ color-mix(in oklab, var(--_sonic-dial-lit) 60%, #000)
164
+ );
165
+ --_sonic-dial-band-inset: calc(var(--_sonic-dial-ring-width) * 0.55);
166
+
167
+ background: conic-gradient(
168
+ from var(--_sonic-dial-start),
169
+ transparent calc(var(--_sonic-dial-modulation-from, 0) * var(--_sonic-dial-sweep)),
170
+ var(--_sonic-dial-modulation) 0
171
+ calc(var(--_sonic-dial-modulation-to, 0) * var(--_sonic-dial-sweep)),
172
+ transparent 0
173
+ );
174
+ }
175
+
176
+ :not([notched]) > .sonic-dial > .sonic-dial-notches,
177
+ :not([modulation]) > .sonic-dial > .sonic-dial-modulation {
178
+ display: none;
179
+ }
180
+
181
+ .sonic-dial-cap {
182
+ position: absolute;
183
+ inset: var(--_sonic-dial-cap-inset);
184
+ background: var(--_sonic-skirt);
185
+ border-radius: 50%;
186
+ box-shadow:
187
+ calc(2 * var(--_sonic-cast-x)) calc(2 * var(--_sonic-cast-y)) calc(4 * var(--_sonic-unit))
188
+ rgb(0 0 0 / 65%),
189
+ var(--_sonic-rim),
190
+ var(--_sonic-focus-shadow);
191
+
192
+ &::before,
193
+ &::after {
194
+ position: absolute;
195
+ content: '';
196
+ border-radius: 50%;
197
+ }
198
+
199
+ &::before {
200
+ inset: 0;
201
+ background: repeating-conic-gradient(
202
+ rgb(0 0 0 / 30%) 0,
203
+ rgb(255 255 255 / 12%) calc(var(--_sonic-dial-ridge-angle) / 2),
204
+ rgb(0 0 0 / 30%) var(--_sonic-dial-ridge-angle)
205
+ );
206
+ rotate: var(--_sonic-dial-angle);
207
+ }
208
+
209
+ &::after {
210
+ inset: 16%;
211
+ background:
212
+ conic-gradient(
213
+ from
214
+ calc(
215
+ var(--_sonic-dial-face-drift) * var(--_sonic-dial-angle) - var(--_sonic-light-tilt)
216
+ ),
217
+ rgb(255 255 255 / 16%),
218
+ transparent 12%,
219
+ rgb(0 0 0 / 10%) 25%,
220
+ transparent 38%,
221
+ rgb(255 255 255 / 12%) 50%,
222
+ transparent 62%,
223
+ rgb(0 0 0 / 10%) 75%,
224
+ transparent 88%,
225
+ rgb(255 255 255 / 16%)
226
+ ),
227
+ var(--_sonic-face);
228
+ box-shadow: var(--_sonic-face-shadow);
229
+ }
230
+ }
231
+
232
+ .sonic-dial-pointer {
233
+ position: absolute;
234
+ inset: var(--_sonic-dial-cap-inset);
235
+ pointer-events: none;
236
+ border-radius: 50%;
237
+ rotate: calc(var(--_sonic-dial-start) + var(--_sonic-dial-angle));
238
+
239
+ /* Evenly shaded, since any offset would turn with the knob */
240
+ &::before {
241
+ --_sonic-dial-pointer-width: calc(var(--_sonic-dial-size) * var(--_sonic-dial-pointer-ratio));
242
+
243
+ position: absolute;
244
+ inset-block-start: 6%;
245
+ inset-inline-start: 50%;
246
+ inline-size: var(--_sonic-dial-pointer-width);
247
+ block-size: 34%;
248
+ content: '';
249
+ background: var(--_sonic-ink);
250
+ border-radius: calc(var(--_sonic-dial-pointer-width) / 2);
251
+ box-shadow: inset 0 0 calc(0.75 * var(--_sonic-unit)) rgb(0 0 0 / 55%);
252
+ clip-path: polygon(
253
+ calc(50% * var(--_sonic-dial-pointer-taper)) 0,
254
+ calc(100% - 50% * var(--_sonic-dial-pointer-taper)) 0,
255
+ 100% 100%,
256
+ 0 100%
257
+ );
258
+ translate: -50% 0;
259
+ }
260
+ }
261
+
262
+ [disabled] > .sonic-dial > .sonic-dial-pointer::before {
263
+ background: var(--_sonic-etch);
264
+ }
265
+ }