@xsynaptic/sonic-ui 0.19.0 → 0.21.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 (79) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +27 -112
  3. package/dist/default/elements/button.js +2 -1
  4. package/dist/default/elements/field-gesture.js +3 -0
  5. package/dist/default/elements/sonic-element.js +1 -1
  6. package/dist/default/elements/split.js +3 -1
  7. package/dist/default/elements/switch.js +5 -3
  8. package/dist/default/elements/toggle.js +10 -5
  9. package/dist/default/elements/value-entry.js +5 -18
  10. package/dist/default/elements/waveform.js +15 -4
  11. package/dist/default/elements/xy.js +33 -5
  12. package/dist/default/lib/canvas-surface.js +5 -1
  13. package/dist/default/lib/double-press.js +30 -0
  14. package/dist/dev/elements/button.js +5 -11
  15. package/dist/dev/elements/button.js.map +1 -1
  16. package/dist/dev/elements/dial.js +2 -12
  17. package/dist/dev/elements/dial.js.map +1 -1
  18. package/dist/dev/elements/display-element.js +1 -5
  19. package/dist/dev/elements/display-element.js.map +1 -1
  20. package/dist/dev/elements/envelope.js +2 -15
  21. package/dist/dev/elements/envelope.js.map +1 -1
  22. package/dist/dev/elements/field-gesture.js +3 -0
  23. package/dist/dev/elements/field-gesture.js.map +1 -1
  24. package/dist/dev/elements/meter.js +1 -1
  25. package/dist/dev/elements/meter.js.map +1 -1
  26. package/dist/dev/elements/number.js +1 -13
  27. package/dist/dev/elements/number.js.map +1 -1
  28. package/dist/dev/elements/segmented.js +3 -10
  29. package/dist/dev/elements/segmented.js.map +1 -1
  30. package/dist/dev/elements/slider.js +2 -12
  31. package/dist/dev/elements/slider.js.map +1 -1
  32. package/dist/dev/elements/sonic-element.d.ts +1 -22
  33. package/dist/dev/elements/sonic-element.js +7 -34
  34. package/dist/dev/elements/sonic-element.js.map +1 -1
  35. package/dist/dev/elements/split.js +3 -1
  36. package/dist/dev/elements/split.js.map +1 -1
  37. package/dist/dev/elements/switch.js +7 -8
  38. package/dist/dev/elements/switch.js.map +1 -1
  39. package/dist/dev/elements/toggle.js +13 -15
  40. package/dist/dev/elements/toggle.js.map +1 -1
  41. package/dist/dev/elements/value-entry.js +5 -18
  42. package/dist/dev/elements/value-entry.js.map +1 -1
  43. package/dist/dev/elements/wave-element.js +2 -12
  44. package/dist/dev/elements/wave-element.js.map +1 -1
  45. package/dist/dev/elements/waveform.d.ts +2 -0
  46. package/dist/dev/elements/waveform.js +15 -4
  47. package/dist/dev/elements/waveform.js.map +1 -1
  48. package/dist/dev/elements/xy.d.ts +4 -0
  49. package/dist/dev/elements/xy.js +35 -17
  50. package/dist/dev/elements/xy.js.map +1 -1
  51. package/dist/dev/lib/canvas-surface.js +5 -1
  52. package/dist/dev/lib/canvas-surface.js.map +1 -1
  53. package/dist/dev/lib/double-press.js +31 -0
  54. package/dist/dev/lib/double-press.js.map +1 -0
  55. package/dist/skins/amber.css +0 -1
  56. package/dist/skins/ivory.css +0 -1
  57. package/dist/skins/lime.css +0 -1
  58. package/dist/skins/slate.css +0 -1
  59. package/dist/styles/button.css +1 -0
  60. package/dist/styles/dial.css +1 -1
  61. package/dist/styles/envelope.css +16 -14
  62. package/dist/styles/led.css +7 -19
  63. package/dist/styles/material/bracket.css +1 -1
  64. package/dist/styles/material/cap.css +27 -4
  65. package/dist/styles/material/core.css +9 -1
  66. package/dist/styles/material/glass.css +1 -7
  67. package/dist/styles/material/lens.css +18 -1
  68. package/dist/styles/material/pane.css +2 -12
  69. package/dist/styles/material/readout.css +7 -1
  70. package/dist/styles/material/well.css +8 -12
  71. package/dist/styles/meter.css +6 -4
  72. package/dist/styles/number.css +8 -9
  73. package/dist/styles/screen.css +1 -3
  74. package/dist/styles/spectrum.css +8 -22
  75. package/dist/styles/toggle.css +4 -0
  76. package/dist/styles/waveform.css +12 -24
  77. package/dist/styles/wavestrip.css +12 -33
  78. package/dist/styles/xy.css +30 -28
  79. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-surface.js","names":["named"],"sources":["../../../src/lib/canvas-surface.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ntype PrivateProperty = `--_sonic-${string}`;\n\nexport interface SurfaceSize {\n\tdpr: number;\n\theight: number;\n\twidth: number;\n}\n\nexport interface SurfaceLook<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n> {\n\tcolours: Record<Colour, string>;\n\tisReducedMotion: boolean;\n\tlengths: Record<Length, number>;\n\tnumbers: Record<Numeric, number>;\n}\n\nexport interface SurfaceFrame<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n> {\n\tframeMs: number;\n\tisDirty: boolean;\n\tisRebuilt: boolean;\n\tlook: SurfaceLook<Colour, Length, Numeric>;\n\tsize: SurfaceSize;\n}\n\nexport interface Surface {\n\tinvalidate: () => void;\n\treadonly isVisible: boolean;\n\trebuild: () => void;\n\trequestFrame: () => void;\n\tsetFill: (isFilling: boolean) => void;\n\treadonly size: SurfaceSize;\n}\n\ninterface SurfaceOptions<Colour extends string, Length extends string, Numeric extends string> {\n\tcanvas: HTMLCanvasElement;\n\tcolours: Record<Colour, PrivateProperty>;\n\tfill?: { control: HTMLElement; sizeProperty: PrivateProperty };\n\tlengths?: Record<Length, PrivateProperty>;\n\tnumbers?: Record<Numeric, PrivateProperty>;\n\tpaint: (context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length, Numeric>) => void;\n\tresize?: (size: SurfaceSize) => void;\n\tsignal: AbortSignal;\n}\n\nfunction deviceRatio(): number {\n\treturn globalThis.devicePixelRatio || 1;\n}\n\n// WebKit reports none, and an emulated device scale reports it in CSS px, so it counts only where it agrees\nfunction deviceBox(\n\tentry: ResizeObserverEntry,\n\tcss: ResizeObserverSize,\n): ResizeObserverSize | undefined {\n\tconst device =\n\t\t'devicePixelContentBoxSize' in entry ? entry.devicePixelContentBoxSize[0] : undefined;\n\tconst dpr = deviceRatio();\n\tif (!device || Math.abs(device.inlineSize - css.inlineSize * dpr) > 1) return undefined;\n\n\treturn Math.abs(device.blockSize - css.blockSize * dpr) > 1 ? undefined : device;\n}\n\n// `Object.fromEntries` widens the keys to `string`\nfunction readEach<Name extends string, Value>(\n\tproperties: Partial<Record<Name, PrivateProperty>>,\n\tread: (property: PrivateProperty) => Value,\n): Record<Name, Value> {\n\tconst named: Readonly<Record<string, PrivateProperty | undefined>> = properties;\n\n\treturn Object.fromEntries(\n\t\tObject.entries(named).flatMap(([name, property]) =>\n\t\t\tproperty === undefined ? [] : [[name, read(property)]],\n\t\t),\n\t) as Record<Name, Value>;\n}\n\nclass CanvasSurface<\n\tColour extends string,\n\tLength extends string,\n\tNumeric extends string,\n> implements Surface {\n\tget isVisible(): boolean {\n\t\treturn this.#isVisible;\n\t}\n\n\tget size(): SurfaceSize {\n\t\treturn this.#size;\n\t}\n\n\t// happy-dom has no canvas context\n\treadonly #context: CanvasRenderingContext2D | undefined;\n\n\t#cssBox: undefined | { height: number; width: number };\n\n\t#fillWatch: ResizeObserver | undefined;\n\n\t#frame: number | undefined;\n\n\t#isDirty = true;\n\n\t#isRebuildWanted = true;\n\n\t#isVisible = true;\n\n\t#look: SurfaceLook<Colour, Length, Numeric> | undefined;\n\n\treadonly #options: SurfaceOptions<Colour, Length, Numeric>;\n\n\treadonly #reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');\n\n\t#size: SurfaceSize = { dpr: deviceRatio(), height: 0, width: 0 };\n\n\tconstructor(options: SurfaceOptions<Colour, Length, Numeric>) {\n\t\tconst { canvas, signal } = options;\n\n\t\tthis.#options = options;\n\t\tthis.#context = canvas.getContext('2d') ?? undefined;\n\t\tthis.#armTransitions();\n\t\tthis.#observeSize();\n\t\tthis.#observeView();\n\t\tthis.#watchResolution();\n\t\tthis.#reducedMotion.addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// `transitionstart` drops events when a change replaces a pending transition in WebKit\n\t\tcanvas.addEventListener(\n\t\t\t'transitionrun',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t\tthis.#stopFill();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t\tthis.#schedule();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.#isDirty = true;\n\t\tthis.#schedule();\n\t}\n\n\trebuild(): void {\n\t\tthis.#isRebuildWanted = true;\n\t\tthis.invalidate();\n\t}\n\n\trequestFrame(): void {\n\t\tthis.#schedule();\n\t}\n\n\tsetFill(isFilling: boolean): void {\n\t\tconst { fill, signal } = this.#options;\n\t\tif (!fill || signal.aborted || isFilling === (this.#fillWatch !== undefined)) return;\n\n\t\tconst { style } = fill.control;\n\n\t\tthis.#stopFill();\n\t\tif (!isFilling) {\n\t\t\tstyle.removeProperty(fill.sizeProperty);\n\t\t\treturn;\n\t\t}\n\n\t\tconst started = new ResizeObserver((entries) => {\n\t\t\tconst box = entries.at(-1)?.borderBoxSize[0];\n\t\t\tif (!box) return;\n\n\t\t\t// Written a frame on, or the canvas resizes inside this delivery and WebKit reports a loop\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\tif (this.#fillWatch === started) {\n\t\t\t\t\tstyle.setProperty(fill.sizeProperty, `${String(box.blockSize)}px`);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\tthis.#fillWatch = started;\n\t\tstarted.observe(fill.control);\n\t}\n\n\t#armTransitions(): void {\n\t\tconst { canvas, colours, numbers = {} } = this.#options;\n\t\tconst { style } = canvas;\n\t\tconst watched = [...Object.values(colours), ...Object.values(numbers)];\n\n\t\tstyle.setProperty('transition-property', watched.join(', '));\n\t\tstyle.setProperty('transition-duration', '0.001ms');\n\t\tstyle.setProperty('transition-timing-function', 'step-start');\n\t\tstyle.setProperty('transition-behavior', 'allow-discrete');\n\t}\n\n\t#current(): { isRebuilt: boolean; look: SurfaceLook<Colour, Length, Numeric> } {\n\t\tif (this.#look && !this.#isRebuildWanted) return { isRebuilt: false, look: this.#look };\n\n\t\treturn { isRebuilt: true, look: this.#read() };\n\t}\n\n\t#draw(frameMs: number): void {\n\t\tthis.#frame = undefined;\n\n\t\tconst size = this.#size;\n\t\tif (size.width === 0 || size.height === 0) return;\n\n\t\tconst { canvas, paint } = this.#options;\n\t\tconst { isRebuilt, look } = this.#current();\n\t\tconst isDirty = this.#isDirty;\n\n\t\tthis.#look = look;\n\t\tthis.#isDirty = false;\n\t\tthis.#isRebuildWanted = false;\n\t\t// Assigning either clears the canvas and its state\n\t\tif (canvas.width !== size.width) canvas.width = size.width;\n\t\tif (canvas.height !== size.height) canvas.height = size.height;\n\t\tif (this.#context) paint(this.#context, { frameMs, isDirty, isRebuilt, look, size });\n\t}\n\n\t#measure(entry: ResizeObserverEntry): void {\n\t\tconst [box] = entry.contentBoxSize;\n\t\tif (!box) return;\n\n\t\tconst device = deviceBox(entry, box);\n\n\t\tif (device) {\n\t\t\tthis.#cssBox = undefined;\n\t\t\tthis.#resize(device.inlineSize, device.blockSize);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#cssBox = { height: box.blockSize, width: box.inlineSize };\n\t\tthis.#rescale();\n\t}\n\n\t#observeSize(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\n\t\t\tif (entry) this.#measure(entry);\n\t\t});\n\n\t\ttry {\n\t\t\tobserver.observe(canvas, { box: 'device-pixel-content-box' });\n\t\t} catch {\n\t\t\t// WebKit throws a TypeError on the option\n\t\t\tobserver.observe(canvas, { box: 'content-box' });\n\t\t}\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\t#observeView(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new IntersectionObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\t\t\tif (!entry) return;\n\n\t\t\tconst wasVisible = this.#isVisible;\n\n\t\t\tthis.#isVisible = entry.isIntersecting;\n\t\t\tif (!entry.isIntersecting && this.#frame !== undefined) {\n\t\t\t\tcancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t}\n\n\t\t\t// Chromium fires no transition inside a skipped `content-visibility` subtree\n\t\t\tif (!wasVisible && entry.isIntersecting) this.rebuild();\n\t\t});\n\n\t\tobserver.observe(canvas);\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\t#read(): SurfaceLook<Colour, Length, Numeric> {\n\t\tconst { canvas, colours, lengths = {}, numbers = {} } = this.#options;\n\t\tconst styles = getComputedStyle(canvas);\n\n\t\treturn {\n\t\t\tcolours: readEach(colours, (property) =>\n\t\t\t\tstyles\n\t\t\t\t\t.getPropertyValue(property)\n\t\t\t\t\t.trim()\n\t\t\t\t\t.replaceAll('currentcolor', () => styles.color),\n\t\t\t),\n\t\t\tisReducedMotion: this.#reducedMotion.matches,\n\t\t\tlengths: readEach<Length, number>(lengths, (property) => readPxProperty(styles, property, 0)),\n\t\t\tnumbers: readEach<Numeric, number>(numbers, (property) =>\n\t\t\t\treadPxProperty(styles, property, 0),\n\t\t\t),\n\t\t};\n\t}\n\n\t#rescale(): void {\n\t\tconst box = this.#cssBox;\n\t\tif (!box) {\n\t\t\tthis.#resize(this.#size.width, this.#size.height);\n\t\t\treturn;\n\t\t}\n\n\t\tconst dpr = deviceRatio();\n\n\t\tthis.#resize(Math.round(box.width * dpr), Math.round(box.height * dpr));\n\t}\n\n\t#resize(width: number, height: number): void {\n\t\tconst previous = this.#size;\n\t\tconst dpr = deviceRatio();\n\t\tif (previous.width === width && previous.height === height && previous.dpr === dpr) return;\n\n\t\tthis.#size = { dpr, height, width };\n\t\tthis.#options.resize?.(this.#size);\n\t\tif (previous.width === 0 || previous.height === 0) this.rebuild();\n\t\telse this.invalidate();\n\t}\n\n\t#schedule(): void {\n\t\tif (this.#options.signal.aborted || this.#frame !== undefined || !this.#isVisible) return;\n\n\t\tthis.#frame = requestAnimationFrame((frameMs) => {\n\t\t\tthis.#draw(frameMs);\n\t\t});\n\t}\n\n\t#stopFill(): void {\n\t\tthis.#fillWatch?.disconnect();\n\t\tthis.#fillWatch = undefined;\n\t}\n\n\t#watchResolution(): void {\n\t\tconst { signal } = this.#options;\n\n\t\tmatchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.#watchResolution();\n\t\t\t\tthis.#rescale();\n\t\t\t},\n\t\t\t{ once: true, signal },\n\t\t);\n\t}\n}\n\nexport function bindSurface<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n>(options: SurfaceOptions<Colour, Length, Numeric>): Surface {\n\treturn new CanvasSurface(options);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"canvas-surface.js","names":["named"],"sources":["../../../src/lib/canvas-surface.ts"],"sourcesContent":["import { readPxProperty } from '#lib/read-px-property.ts';\n\ntype PrivateProperty = `--_sonic-${string}`;\n\nexport interface SurfaceSize {\n\tdpr: number;\n\theight: number;\n\twidth: number;\n}\n\nexport interface SurfaceLook<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n> {\n\tcolours: Record<Colour, string>;\n\tisReducedMotion: boolean;\n\tlengths: Record<Length, number>;\n\tnumbers: Record<Numeric, number>;\n}\n\nexport interface SurfaceFrame<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n> {\n\tframeMs: number;\n\tisDirty: boolean;\n\tisRebuilt: boolean;\n\tlook: SurfaceLook<Colour, Length, Numeric>;\n\tsize: SurfaceSize;\n}\n\nexport interface Surface {\n\tinvalidate: () => void;\n\treadonly isVisible: boolean;\n\trebuild: () => void;\n\trequestFrame: () => void;\n\tsetFill: (isFilling: boolean) => void;\n\treadonly size: SurfaceSize;\n}\n\ninterface SurfaceOptions<Colour extends string, Length extends string, Numeric extends string> {\n\tcanvas: HTMLCanvasElement;\n\tcolours: Record<Colour, PrivateProperty>;\n\tfill?: { control: HTMLElement; sizeProperty: PrivateProperty };\n\tlengths?: Record<Length, PrivateProperty>;\n\tnumbers?: Record<Numeric, PrivateProperty>;\n\tpaint: (context: CanvasRenderingContext2D, frame: SurfaceFrame<Colour, Length, Numeric>) => void;\n\tresize?: (size: SurfaceSize) => void;\n\tsignal: AbortSignal;\n}\n\nfunction deviceRatio(): number {\n\treturn globalThis.devicePixelRatio || 1;\n}\n\n// WebKit reports none, and an emulated device scale reports it in CSS px, so it counts only where it agrees\nfunction deviceBox(\n\tentry: ResizeObserverEntry,\n\tcss: ResizeObserverSize,\n): ResizeObserverSize | undefined {\n\tconst device =\n\t\t'devicePixelContentBoxSize' in entry ? entry.devicePixelContentBoxSize[0] : undefined;\n\tconst dpr = deviceRatio();\n\tif (!device || Math.abs(device.inlineSize - css.inlineSize * dpr) > 1) return undefined;\n\n\treturn Math.abs(device.blockSize - css.blockSize * dpr) > 1 ? undefined : device;\n}\n\n// `Object.fromEntries` widens the keys to `string`\nfunction readEach<Name extends string, Value>(\n\tproperties: Partial<Record<Name, PrivateProperty>>,\n\tread: (property: PrivateProperty) => Value,\n): Record<Name, Value> {\n\tconst named: Readonly<Record<string, PrivateProperty | undefined>> = properties;\n\n\treturn Object.fromEntries(\n\t\tObject.entries(named).flatMap(([name, property]) =>\n\t\t\tproperty === undefined ? [] : [[name, read(property)]],\n\t\t),\n\t) as Record<Name, Value>;\n}\n\nclass CanvasSurface<\n\tColour extends string,\n\tLength extends string,\n\tNumeric extends string,\n> implements Surface {\n\tget isVisible(): boolean {\n\t\treturn this.#isVisible;\n\t}\n\n\tget size(): SurfaceSize {\n\t\treturn this.#size;\n\t}\n\n\t// happy-dom has no canvas context\n\treadonly #context: CanvasRenderingContext2D | undefined;\n\n\t#cssBox: undefined | { height: number; width: number };\n\n\t#fillWatch: ResizeObserver | undefined;\n\n\t#frame: number | undefined;\n\n\t#isDirty = true;\n\n\t#isRebuildWanted = true;\n\n\t#isVisible = true;\n\n\t#look: SurfaceLook<Colour, Length, Numeric> | undefined;\n\n\treadonly #options: SurfaceOptions<Colour, Length, Numeric>;\n\n\treadonly #reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');\n\n\t#size: SurfaceSize = { dpr: deviceRatio(), height: 0, width: 0 };\n\n\tconstructor(options: SurfaceOptions<Colour, Length, Numeric>) {\n\t\tconst { canvas, signal } = options;\n\n\t\tthis.#options = options;\n\t\tthis.#context = canvas.getContext('2d') ?? undefined;\n\t\tthis.#armTransitions();\n\t\tthis.#observeSize();\n\t\tthis.#observeView();\n\t\tthis.#watchResolution();\n\t\tthis.#reducedMotion.addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\t// `transitionstart` drops events when a change replaces a pending transition in WebKit\n\t\tcanvas.addEventListener(\n\t\t\t'transitionrun',\n\t\t\t() => {\n\t\t\t\tthis.rebuild();\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t\tsignal.addEventListener(\n\t\t\t'abort',\n\t\t\t() => {\n\t\t\t\tif (this.#frame !== undefined) cancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t\tthis.#stopFill();\n\t\t\t},\n\t\t\t{ once: true },\n\t\t);\n\t\tthis.#schedule();\n\t}\n\n\tinvalidate(): void {\n\t\tthis.#isDirty = true;\n\t\tthis.#schedule();\n\t}\n\n\trebuild(): void {\n\t\tthis.#isRebuildWanted = true;\n\t\tthis.invalidate();\n\t}\n\n\trequestFrame(): void {\n\t\tthis.#schedule();\n\t}\n\n\tsetFill(isFilling: boolean): void {\n\t\tconst { fill, signal } = this.#options;\n\t\tif (!fill || signal.aborted || isFilling === (this.#fillWatch !== undefined)) return;\n\n\t\tconst { style } = fill.control;\n\n\t\tthis.#stopFill();\n\t\tif (!isFilling) {\n\t\t\tstyle.removeProperty(fill.sizeProperty);\n\t\t\treturn;\n\t\t}\n\n\t\tconst started = new ResizeObserver((entries) => {\n\t\t\tconst box = entries.at(-1)?.borderBoxSize[0];\n\t\t\tif (!box) return;\n\n\t\t\t// Written a frame on, or the canvas resizes inside this delivery and WebKit reports a loop\n\t\t\trequestAnimationFrame(() => {\n\t\t\t\tif (this.#fillWatch === started) {\n\t\t\t\t\tstyle.setProperty(fill.sizeProperty, `${String(box.blockSize)}px`);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\tthis.#fillWatch = started;\n\t\tstarted.observe(fill.control);\n\t}\n\n\t#armTransitions(): void {\n\t\tconst { canvas, colours, numbers = {} } = this.#options;\n\t\tconst { style } = canvas;\n\t\t// A colour mixed from `currentcolor` computes to the same text whatever `color` is\n\t\tconst watched = ['color', ...Object.values(colours), ...Object.values(numbers)];\n\n\t\tstyle.setProperty('transition-property', watched.join(', '));\n\t\tstyle.setProperty('transition-duration', '0.001ms');\n\t\tstyle.setProperty('transition-timing-function', 'step-start');\n\t\tstyle.setProperty('transition-behavior', 'allow-discrete');\n\t}\n\n\t#current(): { isRebuilt: boolean; look: SurfaceLook<Colour, Length, Numeric> } {\n\t\tif (this.#look && !this.#isRebuildWanted) return { isRebuilt: false, look: this.#look };\n\n\t\treturn { isRebuilt: true, look: this.#read() };\n\t}\n\n\t#draw(frameMs: number): void {\n\t\tthis.#frame = undefined;\n\n\t\tconst size = this.#size;\n\t\tif (size.width === 0 || size.height === 0) return;\n\n\t\tconst { canvas, paint } = this.#options;\n\t\tconst { isRebuilt, look } = this.#current();\n\t\tconst isDirty = this.#isDirty;\n\n\t\tthis.#look = look;\n\t\tthis.#isDirty = false;\n\t\tthis.#isRebuildWanted = false;\n\t\t// Assigning either clears the canvas and its state\n\t\tif (canvas.width !== size.width) canvas.width = size.width;\n\t\tif (canvas.height !== size.height) canvas.height = size.height;\n\t\tif (this.#context) paint(this.#context, { frameMs, isDirty, isRebuilt, look, size });\n\t}\n\n\t#measure(entry: ResizeObserverEntry): void {\n\t\tconst [box] = entry.contentBoxSize;\n\t\tif (!box) return;\n\n\t\tconst device = deviceBox(entry, box);\n\n\t\tif (device) {\n\t\t\tthis.#cssBox = undefined;\n\t\t\tthis.#resize(device.inlineSize, device.blockSize);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.#cssBox = { height: box.blockSize, width: box.inlineSize };\n\t\tthis.#rescale();\n\t}\n\n\t#observeSize(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new ResizeObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\n\t\t\tif (entry) this.#measure(entry);\n\t\t});\n\n\t\ttry {\n\t\t\tobserver.observe(canvas, { box: 'device-pixel-content-box' });\n\t\t} catch {\n\t\t\t// WebKit throws a TypeError on the option\n\t\t\tobserver.observe(canvas, { box: 'content-box' });\n\t\t}\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\t#observeView(): void {\n\t\tconst { canvas, signal } = this.#options;\n\t\tconst observer = new IntersectionObserver((entries) => {\n\t\t\tconst entry = entries.at(-1);\n\t\t\tif (!entry) return;\n\n\t\t\tconst wasVisible = this.#isVisible;\n\n\t\t\tthis.#isVisible = entry.isIntersecting;\n\t\t\tif (!entry.isIntersecting && this.#frame !== undefined) {\n\t\t\t\tcancelAnimationFrame(this.#frame);\n\t\t\t\tthis.#frame = undefined;\n\t\t\t}\n\n\t\t\t// Chromium fires no transition inside a skipped `content-visibility` subtree\n\t\t\tif (!wasVisible && entry.isIntersecting) this.rebuild();\n\t\t});\n\n\t\tobserver.observe(canvas);\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\t#read(): SurfaceLook<Colour, Length, Numeric> {\n\t\tconst { canvas, colours, lengths = {}, numbers = {} } = this.#options;\n\t\tconst styles = getComputedStyle(canvas);\n\n\t\treturn {\n\t\t\tcolours: readEach(colours, (property) =>\n\t\t\t\tstyles\n\t\t\t\t\t.getPropertyValue(property)\n\t\t\t\t\t.trim()\n\t\t\t\t\t.replaceAll('currentcolor', () => styles.color),\n\t\t\t),\n\t\t\tisReducedMotion: this.#reducedMotion.matches,\n\t\t\tlengths: readEach<Length, number>(lengths, (property) => readPxProperty(styles, property, 0)),\n\t\t\tnumbers: readEach<Numeric, number>(numbers, (property) =>\n\t\t\t\treadPxProperty(styles, property, 0),\n\t\t\t),\n\t\t};\n\t}\n\n\t#rescale(): void {\n\t\tconst box = this.#cssBox;\n\t\tif (!box) {\n\t\t\tthis.#resize(this.#size.width, this.#size.height);\n\t\t\treturn;\n\t\t}\n\n\t\tconst dpr = deviceRatio();\n\n\t\tthis.#resize(Math.round(box.width * dpr), Math.round(box.height * dpr));\n\t}\n\n\t#resize(width: number, height: number): void {\n\t\tconst previous = this.#size;\n\t\tconst dpr = deviceRatio();\n\t\tif (previous.width === width && previous.height === height && previous.dpr === dpr) return;\n\n\t\tthis.#size = { dpr, height, width };\n\t\tthis.#options.resize?.(this.#size);\n\t\tif (previous.width === 0 || previous.height === 0) this.rebuild();\n\t\telse this.invalidate();\n\t}\n\n\t#schedule(): void {\n\t\tif (this.#options.signal.aborted || this.#frame !== undefined || !this.#isVisible) return;\n\n\t\tthis.#frame = requestAnimationFrame((frameMs) => {\n\t\t\tthis.#draw(frameMs);\n\t\t});\n\t}\n\n\t#stopFill(): void {\n\t\tthis.#fillWatch?.disconnect();\n\t\tthis.#fillWatch = undefined;\n\t}\n\n\t#watchResolution(): void {\n\t\tconst { signal } = this.#options;\n\n\t\tmatchMedia(`(resolution: ${String(deviceRatio())}dppx)`).addEventListener(\n\t\t\t'change',\n\t\t\t() => {\n\t\t\t\tthis.#watchResolution();\n\t\t\t\tthis.#rescale();\n\t\t\t},\n\t\t\t{ once: true, signal },\n\t\t);\n\t}\n}\n\nexport function bindSurface<\n\tColour extends string,\n\tLength extends string = never,\n\tNumeric extends string = never,\n>(options: SurfaceOptions<Colour, Length, Numeric>): Surface {\n\treturn new CanvasSurface(options);\n}\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ //#region src/lib/double-press.ts
2
+ const doublePressMs = 500;
3
+ const doublePressPx = 4;
4
+ function isDoublePress(previous, next) {
5
+ if (!previous) return false;
6
+ return next.time - previous.time < doublePressMs && Math.hypot(next.x - previous.x, next.y - previous.y) < doublePressPx;
7
+ }
8
+ function createDoublePress() {
9
+ let last;
10
+ return {
11
+ forget() {
12
+ last = void 0;
13
+ },
14
+ press(event) {
15
+ const press = {
16
+ time: event.timeStamp,
17
+ x: event.clientX,
18
+ y: event.clientY
19
+ };
20
+ const previous = last;
21
+ last = press;
22
+ if (!isDoublePress(previous, press)) return false;
23
+ last = void 0;
24
+ return true;
25
+ }
26
+ };
27
+ }
28
+
29
+ //#endregion
30
+ export { createDoublePress };
31
+ //# sourceMappingURL=double-press.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"double-press.js","names":[],"sources":["../../../src/lib/double-press.ts"],"sourcesContent":["interface Press {\n\ttime: number;\n\tx: number;\n\ty: number;\n}\n\nexport interface DoublePress {\n\tforget(): void;\n\tpress(event: PointerEvent): boolean;\n}\n\n// Not `dblclick`, which iOS may not deliver under `touch-action: none`\nconst doublePressMs = 500;\nconst doublePressPx = 4;\n\nfunction isDoublePress(previous: Press | undefined, next: Press): boolean {\n\tif (!previous) return false;\n\n\treturn (\n\t\tnext.time - previous.time < doublePressMs &&\n\t\tMath.hypot(next.x - previous.x, next.y - previous.y) < doublePressPx\n\t);\n}\n\nexport function createDoublePress(): DoublePress {\n\tlet last: Press | undefined;\n\n\treturn {\n\t\tforget() {\n\t\t\tlast = undefined;\n\t\t},\n\t\tpress(event) {\n\t\t\tconst press = { time: event.timeStamp, x: event.clientX, y: event.clientY };\n\t\t\tconst previous = last;\n\n\t\t\tlast = press;\n\t\t\tif (!isDoublePress(previous, press)) return false;\n\n\t\t\tlast = undefined;\n\n\t\t\treturn true;\n\t\t},\n\t};\n}\n"],"mappings":";AAYA,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AAEtB,SAAS,cAAc,UAA6B,MAAsB;CACzE,IAAI,CAAC,UAAU,OAAO;CAEtB,OACC,KAAK,OAAO,SAAS,OAAO,iBAC5B,KAAK,MAAM,KAAK,IAAI,SAAS,GAAG,KAAK,IAAI,SAAS,CAAC,IAAI;AAEzD;AAEA,SAAgB,oBAAiC;CAChD,IAAI;CAEJ,OAAO;EACN,SAAS;GACR,OAAO;EACR;EACA,MAAM,OAAO;GACZ,MAAM,QAAQ;IAAE,MAAM,MAAM;IAAW,GAAG,MAAM;IAAS,GAAG,MAAM;GAAQ;GAC1E,MAAM,WAAW;GAEjB,OAAO;GACP,IAAI,CAAC,cAAc,UAAU,KAAK,GAAG,OAAO;GAE5C,OAAO;GAEP,OAAO;EACR;CACD;AACD"}
@@ -4,6 +4,5 @@
4
4
  --sonic-cap: oklch(37% 0.012 286deg);
5
5
  --sonic-cap-hover: oklch(44.2% 0.015 286deg);
6
6
  --sonic-ink: color-mix(in oklab, var(--sonic-cap) 30%, #fff);
7
- --sonic-wavestrip-inset-ratio: 0.1;
8
7
  }
9
8
  }
@@ -4,6 +4,5 @@
4
4
  --sonic-cap: oklch(85.5% 0.024 85deg);
5
5
  --sonic-cap-hover: oklch(90.5% 0.02 85deg);
6
6
  --sonic-ink: oklch(18.5% 0.012 60deg);
7
- --sonic-wavestrip-inset-ratio: 0.1;
8
7
  }
9
8
  }
@@ -8,6 +8,5 @@
8
8
  --sonic-ink: color-mix(in oklab, var(--sonic-cap) 30%, #fff);
9
9
  --sonic-dial-ring-ratio: 0.05;
10
10
  --sonic-dial-gap-ratio: 0.1;
11
- --sonic-wavestrip-inset-ratio: 0.1;
12
11
  }
13
12
  }
@@ -8,6 +8,5 @@
8
8
  --sonic-ink: color-mix(in oklab, var(--sonic-cap) 30%, #fff);
9
9
  --sonic-dial-ring-ratio: 0.12;
10
10
  --sonic-dial-gap-ratio: 0.07;
11
- --sonic-wavestrip-inset-ratio: 0.1;
12
11
  }
13
12
  }
@@ -85,6 +85,7 @@
85
85
  font: inherit;
86
86
  color: var(--_sonic-ink);
87
87
  cursor: pointer;
88
+ user-select: none;
88
89
  background: var(--_sonic-well-floor);
89
90
  border: 0;
90
91
  border-radius: var(--_sonic-button-corner);
@@ -81,7 +81,7 @@
81
81
  flex-shrink: 0;
82
82
  inline-size: var(--_sonic-dial-size);
83
83
  block-size: var(--_sonic-dial-size);
84
- touch-action: pan-x;
84
+ touch-action: var(--sonic-touch-action, pan-x);
85
85
  cursor: ns-resize;
86
86
  user-select: none;
87
87
  border-radius: 50%;
@@ -24,7 +24,6 @@
24
24
  }
25
25
 
26
26
  .sonic-envelope {
27
- --_sonic-envelope-radius-ratio: var(--sonic-envelope-radius-ratio, 0.08);
28
27
  --_sonic-envelope-inset-ratio: var(--sonic-envelope-inset-ratio, 0.1);
29
28
  --_sonic-envelope-handle-ratio: var(--sonic-envelope-handle-ratio, 0.14);
30
29
  --_sonic-envelope-handle-stroke-ratio: var(--sonic-envelope-handle-stroke-ratio, 0.025);
@@ -32,18 +31,16 @@
32
31
  --_sonic-envelope-curve-ratio: var(--sonic-envelope-curve-ratio, 0.08);
33
32
  --_sonic-unit: calc(var(--_sonic-envelope-size) / 64);
34
33
  --_sonic-envelope-inset: calc(var(--_sonic-envelope-size) * var(--_sonic-envelope-inset-ratio));
35
- --_sonic-envelope-radius-own: calc(
36
- var(--_sonic-envelope-size) * var(--sonic-envelope-radius-ratio)
37
- );
38
- --_sonic-glass-radius: var(
39
- --_sonic-envelope-radius-own,
40
- var(--sonic-radius, calc(var(--_sonic-envelope-size) * var(--_sonic-envelope-radius-ratio)))
41
- );
42
34
 
35
+ /* fallow-ignore-next-line code-duplication -- a plain box restated, since sheets never import each other */
36
+ position: relative;
37
+ display: grid;
38
+ flex-shrink: 0;
39
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
43
40
  inline-size: calc(var(--_sonic-envelope-size) * var(--_sonic-envelope-aspect-ratio));
44
41
  block-size: var(--_sonic-envelope-size);
45
42
  container-type: size;
46
- border-radius: var(--_sonic-glass-radius);
43
+ user-select: none;
47
44
  isolation: isolate;
48
45
  }
49
46
 
@@ -68,7 +65,6 @@
68
65
  inset: 0;
69
66
  overflow: clip;
70
67
  pointer-events: none;
71
- border-radius: inherit;
72
68
 
73
69
  &[data-sonic-part='curve'] {
74
70
  --_sonic-point-size: calc(
@@ -160,8 +156,8 @@
160
156
  }
161
157
 
162
158
  :is(.sonic-envelope-curve, .sonic-envelope-handle)[data-sonic-disabled] {
163
- --_sonic-point-ring: var(--_sonic-unlit);
164
- --_sonic-point-core: var(--_sonic-glass);
159
+ --_sonic-point-ring: var(--_sonic-unlit-solid);
160
+ --_sonic-point-core: var(--_sonic-point-ring);
165
161
 
166
162
  cursor: default;
167
163
  }
@@ -175,7 +171,7 @@
175
171
 
176
172
  :is(.sonic-envelope-curve, .sonic-envelope-handle):not([data-sonic-disabled]):active {
177
173
  --_sonic-point-ring: var(--_sonic-lit);
178
- --_sonic-point-core: var(--_sonic-glass);
174
+ --_sonic-point-core: var(--_sonic-point-ring);
179
175
  --_sonic-point-halo: color-mix(
180
176
  in oklab,
181
177
  var(--_sonic-lit) calc(50% * var(--_sonic-relief)),
@@ -187,7 +183,7 @@
187
183
  .sonic-envelope {
188
184
  --_sonic-lit: Highlight;
189
185
  --_sonic-unlit: GrayText;
190
- --_sonic-glass: Canvas;
186
+ --_sonic-unlit-solid: GrayText;
191
187
  }
192
188
 
193
189
  .sonic-envelope-graph {
@@ -205,6 +201,12 @@
205
201
  --_sonic-point-core: Canvas;
206
202
  }
207
203
 
204
+ /* Spelt as the rules above are, to outrank them */
205
+ :is(.sonic-envelope-curve, .sonic-envelope-handle)[data-sonic-disabled],
206
+ :is(.sonic-envelope-curve, .sonic-envelope-handle):not([data-sonic-disabled]):active {
207
+ --_sonic-point-core: Canvas;
208
+ }
209
+
208
210
  .sonic-envelope-fill {
209
211
  fill: none;
210
212
  }
@@ -11,30 +11,13 @@
11
11
  --_sonic-led-size: var(--sonic-led-size, 0.5rem);
12
12
  --_sonic-led-lens-ratio: var(--sonic-led-lens-ratio, calc(0.8 - 0.175 * var(--_sonic-relief)));
13
13
  --_sonic-unit: calc(var(--_sonic-led-size) / 8);
14
- --_sonic-led-modelled: calc(100% * var(--_sonic-relief));
15
- --_sonic-led-bezel: var(
16
- --sonic-bezel,
17
- color-mix(
18
- in oklab,
19
- color-mix(in oklab, var(--_sonic-unlit) 55%, #000) var(--_sonic-led-modelled),
20
- transparent
21
- )
22
- );
23
- --_sonic-lens-glass: var(
24
- --sonic-lens,
25
- color-mix(
26
- in oklab,
27
- color-mix(in oklab, var(--_sonic-unlit) 60%, #000) var(--_sonic-led-modelled),
28
- color-mix(in oklab, currentcolor 30%, transparent)
29
- )
30
- );
31
14
 
32
15
  position: relative;
33
16
  display: inline-block;
34
17
  flex-shrink: 0;
35
18
  inline-size: var(--_sonic-led-size);
36
19
  block-size: var(--_sonic-led-size);
37
- background: var(--_sonic-led-bezel);
20
+ background: var(--_sonic-lens-bezel);
38
21
  border-radius: 50%;
39
22
 
40
23
  &::after {
@@ -112,7 +95,7 @@
112
95
  calc(var(--_sonic-led-lens-ratio) * 100%),
113
96
  transparent
114
97
  ),
115
- var(--_sonic-led-bezel);
98
+ var(--_sonic-lens-bezel);
116
99
 
117
100
  &::after {
118
101
  background: radial-gradient(
@@ -135,4 +118,9 @@
135
118
  }
136
119
  }
137
120
  }
121
+
122
+ :is(.sonic-button:is(:disabled, [aria-disabled='true']), .sonic-segmented-option:disabled)
123
+ .sonic-led {
124
+ --_sonic-lit: inherit;
125
+ }
138
126
  }
@@ -7,7 +7,7 @@
7
7
  pointer-events: none;
8
8
  content: '';
9
9
  border: max(1px, 0.06 * var(--_sonic-bracket-size)) solid
10
- color-mix(in oklab, var(--_sonic-glass-text) 45%, transparent);
10
+ color-mix(in oklab, currentcolor 45%, transparent);
11
11
  border-radius: 50%;
12
12
  opacity: 0;
13
13
  mask: conic-gradient(
@@ -48,7 +48,7 @@
48
48
  )
49
49
  );
50
50
  --_sonic-sheen: linear-gradient(
51
- 105deg,
51
+ calc(var(--_sonic-light-fall) - 58deg),
52
52
  transparent 20%,
53
53
  rgb(from var(--_sonic-shine) r g b / calc(0.1 * var(--_sonic-relief))) 45%,
54
54
  transparent 55%,
@@ -80,7 +80,12 @@
80
80
  }
81
81
 
82
82
  :is(.sonic-number, .sonic-waveform, .sonic-wavestrip, .sonic-xy) {
83
- --_sonic-ink: var(--_sonic-glass-text);
83
+ --_sonic-ink: currentcolor;
84
+ }
85
+
86
+ /* A plain box has no corner of its own, so its focus outline and glow take this one */
87
+ :is(.sonic-number, .sonic-slider, .sonic-switch-position, .sonic-waveform, .sonic-wavestrip) {
88
+ border-radius: var(--sonic-focus-radius, 0);
84
89
  }
85
90
 
86
91
  :is(.sonic-dial, .sonic-slider, .sonic-waveform, .sonic-wavestrip) {
@@ -143,13 +148,31 @@
143
148
  outline-offset: var(--sonic-focus-outline-offset, -2px);
144
149
 
145
150
  &:not([data-sonic-pointer-focus]) {
146
- outline: var(--sonic-focus-outline, 2px solid transparent);
147
-
148
151
  --_sonic-cap: var(--_sonic-cap-hover);
149
152
  --_sonic-focus-shadow: var(--_sonic-focus-glow);
150
153
  }
151
154
  }
152
155
 
156
+ :is(
157
+ .sonic-number,
158
+ .sonic-slider,
159
+ .sonic-waveform,
160
+ .sonic-wavestrip,
161
+ .sonic-xy-axis
162
+ ):focus-visible:not([data-sonic-pointer-focus]) {
163
+ outline: var(--sonic-focus-outline, 2px solid transparent);
164
+ }
165
+
166
+ @media not (forced-colors: active) {
167
+ :is(.sonic-dial, .sonic-button, .sonic-segmented-option):focus-visible:not(
168
+ [data-sonic-pointer-focus]
169
+ )
170
+ > :is(.sonic-dial-cap, .sonic-button-cap, .sonic-segmented-cap) {
171
+ outline: var(--sonic-focus-outline, 2px solid transparent);
172
+ outline-offset: var(--sonic-focus-outline-offset, -2px);
173
+ }
174
+ }
175
+
153
176
  :state(editing)
154
177
  > :is(.sonic-dial, .sonic-number, .sonic-slider, .sonic-waveform, .sonic-wavestrip) {
155
178
  --_sonic-cap: var(--_sonic-cap-hover);
@@ -30,8 +30,16 @@
30
30
  .sonic-xy,
31
31
  .sonic-xy-readout
32
32
  ) {
33
- --_sonic-unlit: var(--sonic-unlit, oklch(27.4% 0.005 286deg));
34
33
  --_sonic-relief: var(--sonic-relief, 0);
34
+ --_sonic-unlit-solid: var(--sonic-unlit, oklch(27.4% 0.005 286deg));
35
+ --_sonic-unlit: var(
36
+ --sonic-unlit,
37
+ color-mix(
38
+ in oklab,
39
+ oklch(27.4% 0.005 286deg) calc(100% * var(--_sonic-relief)),
40
+ color-mix(in oklab, currentcolor 30%, transparent)
41
+ )
42
+ );
35
43
  --_sonic-shade: var(--sonic-shade, #000);
36
44
  --_sonic-shine: var(--sonic-shine, #fff);
37
45
  --_sonic-press-duration: var(--sonic-press-duration, 60ms);
@@ -1,20 +1,14 @@
1
1
  @layer sonic.material {
2
2
  :is(
3
3
  .sonic-dial-readout,
4
- .sonic-envelope,
5
4
  .sonic-envelope-readout,
6
- .sonic-number,
7
5
  .sonic-screen,
8
6
  .sonic-slider-readout,
9
- .sonic-spectrum,
10
- .sonic-waveform,
11
7
  .sonic-waveform-readout,
12
- .sonic-wavestrip,
13
8
  .sonic-wavestrip-readout,
14
- .sonic-xy,
15
9
  .sonic-xy-readout
16
10
  ) {
17
- --_sonic-glass-slab: color-mix(in oklab, var(--_sonic-unlit) 45%, #000);
11
+ --_sonic-glass-slab: color-mix(in oklab, var(--_sonic-unlit-solid) 45%, #000);
18
12
  --_sonic-glass-base: var(
19
13
  --sonic-glass,
20
14
  color-mix(
@@ -5,7 +5,24 @@
5
5
  rgb(from var(--_sonic-shine) r g b / calc(0.1 * var(--_sonic-relief))),
6
6
  transparent 60%
7
7
  );
8
- --_sonic-lens-glass: var(--sonic-lens, color-mix(in oklab, var(--_sonic-unlit) 60%, #000));
8
+ --_sonic-lens-modelled: calc(100% * var(--_sonic-relief));
9
+ --_sonic-lens-tint: color-mix(in oklab, currentcolor 30%, transparent);
10
+ --_sonic-lens-bezel: var(
11
+ --sonic-bezel,
12
+ color-mix(
13
+ in oklab,
14
+ color-mix(in oklab, var(--_sonic-unlit) 55%, #000) var(--_sonic-lens-modelled),
15
+ transparent
16
+ )
17
+ );
18
+ --_sonic-lens-glass: var(
19
+ --sonic-lens,
20
+ color-mix(
21
+ in oklab,
22
+ color-mix(in oklab, var(--_sonic-unlit) 60%, #000) var(--_sonic-lens-modelled),
23
+ var(--_sonic-lens-tint)
24
+ )
25
+ );
9
26
  --_sonic-lens: var(--_sonic-lens-glint), var(--_sonic-lens-glass);
10
27
  }
11
28
  }
@@ -7,15 +7,7 @@
7
7
  @layer sonic.material {
8
8
  /* A definite cell, so percentage-sized content fills the glass; a canvas sized to its own box would feed back */
9
9
 
10
- :is(
11
- .sonic-envelope,
12
- .sonic-number,
13
- .sonic-screen,
14
- .sonic-spectrum,
15
- .sonic-waveform,
16
- .sonic-wavestrip,
17
- .sonic-xy
18
- ) {
10
+ .sonic-screen {
19
11
  --_sonic-glass-depth: var(--sonic-glass-depth, var(--_sonic-relief));
20
12
  --_sonic-glass-texture: var(--sonic-glass-texture, var(--_sonic-relief));
21
13
  --_sonic-glass-pitch: max(2px, round(2 * var(--_sonic-unit), 1px));
@@ -23,12 +15,10 @@
23
15
  position: relative;
24
16
  box-sizing: border-box;
25
17
  display: grid;
26
- flex-shrink: 0;
27
18
  grid-template: minmax(0, 1fr) / minmax(0, 1fr);
28
19
  place-items: center;
29
20
  font-family: var(--sonic-glass-font, inherit);
30
21
  color: var(--_sonic-glass-text);
31
- user-select: none;
32
22
  background: var(--_sonic-glass-fill);
33
23
  border: var(--_sonic-glass-edge);
34
24
 
@@ -83,7 +73,7 @@
83
73
 
84
74
  @media (prefers-reduced-motion: no-preference) {
85
75
  &:is(:focus-visible, :focus-within)::before,
86
- :state(dragging) > &::before {
76
+ &:has(:state(dragging))::before {
87
77
  animation: sonic-glass-breathe 3s ease-in-out infinite alternate;
88
78
  }
89
79
 
@@ -18,6 +18,9 @@
18
18
  .sonic-xy-readout
19
19
  ) {
20
20
  --_sonic-unit: calc(var(--_sonic-readout-size) / 32);
21
+ --_sonic-readout-radius-own: calc(
22
+ var(--_sonic-readout-size) * var(--sonic-readout-radius-ratio)
23
+ );
21
24
  --_sonic-glass-own: var(--sonic-readout-glass);
22
25
  --_sonic-glass-edge-colour-own: var(--sonic-readout-edge);
23
26
  --_sonic-glass-edge-width-own: var(--sonic-readout-edge-width);
@@ -43,7 +46,10 @@
43
46
  user-select: none;
44
47
  background: var(--_sonic-glass-fill);
45
48
  border: var(--_sonic-glass-edge);
46
- border-radius: calc(0.25 * var(--_sonic-readout-size));
49
+ border-radius: var(
50
+ --_sonic-readout-radius-own,
51
+ var(--sonic-radius, calc(0.25 * var(--_sonic-readout-size)))
52
+ );
47
53
  box-shadow:
48
54
  calc(2 * var(--_sonic-cast-x)) calc(2 * var(--_sonic-cast-y)) calc(6 * var(--_sonic-unit))
49
55
  rgb(from var(--_sonic-shade) r g b / calc(0.55 * var(--_sonic-relief))),
@@ -1,17 +1,13 @@
1
1
  @layer sonic.material {
2
- :is(
3
- .sonic-envelope,
4
- .sonic-button,
5
- .sonic-number,
6
- .sonic-screen,
7
- .sonic-segmented,
8
- .sonic-spectrum,
9
- .sonic-toggle,
10
- .sonic-waveform,
11
- .sonic-wavestrip,
12
- .sonic-xy
13
- ) {
2
+ :is(.sonic-button, .sonic-segmented, .sonic-toggle) {
3
+ --_sonic-well-depth: 1;
4
+ }
5
+
6
+ .sonic-screen {
14
7
  --_sonic-well-depth: var(--_sonic-glass-depth, 1);
8
+ }
9
+
10
+ :is(.sonic-button, .sonic-screen, .sonic-segmented, .sonic-toggle) {
15
11
  --_sonic-well:
16
12
  inset calc(var(--_sonic-well-depth) * var(--_sonic-cast-x))
17
13
  calc(var(--_sonic-well-depth) * var(--_sonic-cast-y))
@@ -49,6 +49,7 @@
49
49
  --_sonic-meter-clip-from: var(--sonic-meter-clip-from, -3);
50
50
  --_sonic-unit: calc(var(--_sonic-meter-size) / 8);
51
51
  --_sonic-meter-toward: to top;
52
+ --_sonic-meter-unlit: var(--_sonic-unlit);
52
53
  --_sonic-meter-inset: calc(var(--_sonic-meter-size) * 0.125);
53
54
  --_sonic-meter-lens: calc(var(--_sonic-meter-size) - 2 * var(--_sonic-meter-inset));
54
55
  --_sonic-meter-clip-space: calc(var(--_sonic-meter-inset) + var(--_sonic-meter-lens));
@@ -63,7 +64,7 @@
63
64
  inline-size: var(--_sonic-meter-size);
64
65
  block-size: var(--_sonic-meter-length);
65
66
  container-type: size;
66
- background: var(--sonic-bezel, color-mix(in oklab, var(--_sonic-unlit) 55%, #000));
67
+ background: var(--_sonic-lens-bezel);
67
68
  border-radius: calc(var(--_sonic-meter-size) * var(--_sonic-meter-radius-ratio));
68
69
 
69
70
  &::after {
@@ -286,10 +287,11 @@
286
287
  .sonic-meter-segments {
287
288
  background: linear-gradient(
288
289
  var(--_sonic-meter-toward),
289
- color-mix(in oklab, var(--_sonic-lit) 12%, var(--_sonic-unlit)) 0 var(--_sonic-meter-hot-at),
290
- color-mix(in oklab, var(--_sonic-lit-warning) 12%, var(--_sonic-unlit)) 0
290
+ color-mix(in oklab, var(--_sonic-lit) 12%, var(--_sonic-meter-unlit)) 0
291
+ var(--_sonic-meter-hot-at),
292
+ color-mix(in oklab, var(--_sonic-lit-warning) 12%, var(--_sonic-meter-unlit)) 0
291
293
  var(--_sonic-meter-clip-at),
292
- color-mix(in oklab, var(--_sonic-lit-danger) 12%, var(--_sonic-unlit)) 0
294
+ color-mix(in oklab, var(--_sonic-lit-danger) 12%, var(--_sonic-meter-unlit)) 0
293
295
  );
294
296
  }
295
297
 
@@ -30,25 +30,24 @@
30
30
  }
31
31
 
32
32
  .sonic-number {
33
- --_sonic-number-radius-ratio: var(--sonic-number-radius-ratio, 0.15);
34
- --_sonic-number-radius-own: calc(var(--_sonic-number-size) * var(--sonic-number-radius-ratio));
35
- --_sonic-number-inset-ratio: var(--sonic-number-inset-ratio, 0.2);
36
33
  --_sonic-number-text-ratio: var(--sonic-number-text-ratio, 0.45);
37
34
  --_sonic-number-travel: var(--sonic-number-travel, 160px);
38
35
  --_sonic-unit: calc(var(--_sonic-number-size) / 32);
39
36
 
37
+ /* fallow-ignore-next-line code-duplication -- a plain box restated, since sheets never import each other */
38
+ position: relative;
39
+ display: grid;
40
+ flex-shrink: 0;
41
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
42
+ place-items: center;
40
43
  inline-size: calc(var(--_sonic-number-size) * var(--_sonic-number-aspect-ratio));
41
44
  block-size: var(--_sonic-number-size);
42
- padding: 0 calc(var(--_sonic-number-size) * var(--_sonic-number-inset-ratio));
43
45
  font-size: calc(var(--_sonic-number-size) * var(--_sonic-number-text-ratio));
44
46
  font-variant-numeric: tabular-nums;
45
47
  line-height: 1;
46
- touch-action: pan-x;
48
+ touch-action: var(--sonic-touch-action, pan-x);
47
49
  cursor: ns-resize;
48
- border-radius: var(
49
- --_sonic-number-radius-own,
50
- var(--sonic-radius, calc(var(--_sonic-number-size) * var(--_sonic-number-radius-ratio)))
51
- );
50
+ user-select: none;
52
51
  box-shadow: var(--_sonic-focus-shadow);
53
52
  }
54
53
 
@@ -9,14 +9,12 @@
9
9
  @layer sonic.control {
10
10
  .sonic-screen {
11
11
  --_sonic-screen-size: var(--sonic-screen-size, 2.5rem);
12
- --_sonic-screen-aspect-ratio: var(--sonic-screen-aspect-ratio, 3);
13
12
  --_sonic-screen-radius-ratio: var(--sonic-screen-radius-ratio, 0.15);
14
13
  --_sonic-screen-radius-own: calc(var(--_sonic-screen-size) * var(--sonic-screen-radius-ratio));
15
14
  --_sonic-screen-inset-ratio: var(--sonic-screen-inset-ratio, 0.1);
16
15
  --_sonic-unit: calc(var(--_sonic-screen-size) / 32);
17
16
 
18
- inline-size: calc(var(--_sonic-screen-size) * var(--_sonic-screen-aspect-ratio));
19
- block-size: var(--_sonic-screen-size);
17
+ min-block-size: var(--_sonic-screen-size);
20
18
  padding: calc(var(--_sonic-screen-size) * var(--_sonic-screen-inset-ratio));
21
19
  border-radius: var(
22
20
  --_sonic-screen-radius-own,
@@ -71,34 +71,21 @@
71
71
  --_sonic-spectrum-lit: var(--_sonic-lit);
72
72
  --_sonic-spectrum-hot: var(--_sonic-lit-warning);
73
73
  --_sonic-spectrum-clip: var(--_sonic-lit-danger);
74
- --_sonic-spectrum-grid: color-mix(in oklab, var(--_sonic-glass-text) 20%, transparent);
74
+ --_sonic-spectrum-grid: color-mix(in oklab, currentcolor 20%, transparent);
75
75
  --_sonic-spectrum-hot-from: var(--sonic-spectrum-hot-from, -12);
76
76
  --_sonic-spectrum-clip-from: var(--sonic-spectrum-clip-from, -3);
77
77
 
78
+ /* fallow-ignore-next-line code-duplication -- a plain box restated, since sheets never import each other */
79
+ position: relative;
80
+ display: grid;
81
+ flex-shrink: 0;
82
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
78
83
  inline-size: var(--_sonic-spectrum-length);
79
84
  block-size: var(--_sonic-spectrum-size);
80
85
 
81
- --_sonic-spectrum-inset: calc(
82
- var(--_sonic-spectrum-size) * var(--sonic-spectrum-inset-ratio, 0.06)
83
- );
84
-
85
- padding: var(--_sonic-spectrum-inset);
86
-
87
86
  /* The canvas's backing size would otherwise feed an intrinsically sized parent */
88
87
  contain: size;
89
-
90
- --_sonic-spectrum-radius-own: calc(
91
- var(--_sonic-spectrum-size) * var(--sonic-spectrum-radius-ratio)
92
- );
93
- --_sonic-glass-radius: var(
94
- --_sonic-spectrum-radius-own,
95
- var(
96
- --sonic-radius,
97
- calc(var(--_sonic-spectrum-size) * var(--sonic-spectrum-radius-ratio, 0.08))
98
- )
99
- );
100
-
101
- border-radius: var(--_sonic-glass-radius);
88
+ user-select: none;
102
89
  }
103
90
 
104
91
  [fill] > .sonic-spectrum,
@@ -110,10 +97,9 @@
110
97
  display: block;
111
98
  inline-size: 100%;
112
99
  block-size: 100%;
113
- border-radius: max(0px, var(--_sonic-glass-radius) - var(--_sonic-spectrum-inset));
114
100
  }
115
101
 
116
- /* Opaque, since a system highlight carries alpha that would blend the bars into the glass */
102
+ /* Opaque, since a system highlight carries alpha that would blend the bars into what is behind them */
117
103
  @media (forced-colors: active) {
118
104
  .sonic-spectrum {
119
105
  --_sonic-spectrum-lit: rgb(from CanvasText r g b / 100%);