@xsynaptic/sonic-ui 0.8.0 → 0.10.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 (74) hide show
  1. package/README.md +0 -2
  2. package/dist/default/elements/dial.js +12 -3
  3. package/dist/default/elements/display-element.js +14 -2
  4. package/dist/default/elements/envelope.js +2 -2
  5. package/dist/default/elements/modulation.js +45 -0
  6. package/dist/default/elements/readout-claim.js +8 -3
  7. package/dist/default/elements/slider.js +16 -18
  8. package/dist/default/elements/spectrum.js +1 -0
  9. package/dist/default/elements/value-element.js +108 -330
  10. package/dist/default/elements/value-gestures.js +301 -0
  11. package/dist/default/elements/wave-element.js +5 -26
  12. package/dist/default/elements/waveform.js +6 -2
  13. package/dist/default/elements/wavestrip.js +5 -16
  14. package/dist/default/elements/xy.js +9 -20
  15. package/dist/default/lib/asked-value.js +21 -0
  16. package/dist/default/lib/canvas-surface.js +3 -0
  17. package/dist/default/lib/fill-watch.js +29 -0
  18. package/dist/default/lib/hover-preview.js +8 -3
  19. package/dist/default/lib/marker-rider.js +6 -3
  20. package/dist/default/lib/read-ms-property.js +10 -0
  21. package/dist/dev/elements/dial.js +12 -3
  22. package/dist/dev/elements/dial.js.map +1 -1
  23. package/dist/dev/elements/display-element.d.ts +4 -1
  24. package/dist/dev/elements/display-element.js +14 -2
  25. package/dist/dev/elements/display-element.js.map +1 -1
  26. package/dist/dev/elements/envelope.js +2 -2
  27. package/dist/dev/elements/envelope.js.map +1 -1
  28. package/dist/dev/elements/modulation.js +46 -0
  29. package/dist/dev/elements/modulation.js.map +1 -0
  30. package/dist/dev/elements/readout-claim.js +8 -3
  31. package/dist/dev/elements/readout-claim.js.map +1 -1
  32. package/dist/dev/elements/slider.d.ts +0 -1
  33. package/dist/dev/elements/slider.js +16 -18
  34. package/dist/dev/elements/slider.js.map +1 -1
  35. package/dist/dev/elements/spectrum.d.ts +1 -0
  36. package/dist/dev/elements/spectrum.js +1 -0
  37. package/dist/dev/elements/spectrum.js.map +1 -1
  38. package/dist/dev/elements/value-element.d.ts +11 -13
  39. package/dist/dev/elements/value-element.js +108 -330
  40. package/dist/dev/elements/value-element.js.map +1 -1
  41. package/dist/dev/elements/value-gestures.d.ts +10 -0
  42. package/dist/dev/elements/value-gestures.js +302 -0
  43. package/dist/dev/elements/value-gestures.js.map +1 -0
  44. package/dist/dev/elements/wave-element.js +5 -26
  45. package/dist/dev/elements/wave-element.js.map +1 -1
  46. package/dist/dev/elements/waveform.js +6 -2
  47. package/dist/dev/elements/waveform.js.map +1 -1
  48. package/dist/dev/elements/wavestrip.d.ts +0 -1
  49. package/dist/dev/elements/wavestrip.js +5 -16
  50. package/dist/dev/elements/wavestrip.js.map +1 -1
  51. package/dist/dev/elements/xy.js +9 -20
  52. package/dist/dev/elements/xy.js.map +1 -1
  53. package/dist/dev/lib/asked-value.js +22 -0
  54. package/dist/dev/lib/asked-value.js.map +1 -0
  55. package/dist/dev/lib/canvas-surface.d.ts +1 -0
  56. package/dist/dev/lib/canvas-surface.js +3 -0
  57. package/dist/dev/lib/canvas-surface.js.map +1 -1
  58. package/dist/dev/lib/fill-watch.js +30 -0
  59. package/dist/dev/lib/fill-watch.js.map +1 -0
  60. package/dist/dev/lib/hover-preview.js +8 -3
  61. package/dist/dev/lib/hover-preview.js.map +1 -1
  62. package/dist/dev/lib/marker-rider.js +6 -3
  63. package/dist/dev/lib/marker-rider.js.map +1 -1
  64. package/dist/dev/lib/read-ms-property.js +11 -0
  65. package/dist/dev/lib/read-ms-property.js.map +1 -0
  66. package/dist/styles/envelope.css +1 -1
  67. package/dist/styles/led.css +1 -1
  68. package/dist/styles/material.css +42 -8
  69. package/dist/styles/spectrum.css +5 -0
  70. package/dist/styles/switch.css +42 -33
  71. package/dist/styles/waveform.css +31 -2
  72. package/dist/styles/wavestrip.css +13 -7
  73. package/dist/styles/xy.css +4 -4
  74. 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\trebuild: () => void;\n\trequestFrame: () => 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\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 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#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},\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\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#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\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\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#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},\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\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#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,30 @@
1
+ //#region src/lib/fill-watch.ts
2
+ function bindFill(control, sizeProperty, signal) {
3
+ let watch;
4
+ const stop = () => {
5
+ watch?.disconnect();
6
+ watch = void 0;
7
+ };
8
+ signal.addEventListener("abort", stop, { once: true });
9
+ return (isFilling) => {
10
+ if (signal.aborted || isFilling === (watch !== void 0)) return;
11
+ stop();
12
+ if (!isFilling) {
13
+ control.style.removeProperty(sizeProperty);
14
+ return;
15
+ }
16
+ const started = new ResizeObserver((entries) => {
17
+ const box = entries.at(-1)?.borderBoxSize[0];
18
+ if (!box) return;
19
+ requestAnimationFrame(() => {
20
+ if (watch === started) control.style.setProperty(sizeProperty, `${String(box.blockSize)}px`);
21
+ });
22
+ });
23
+ watch = started;
24
+ started.observe(control);
25
+ };
26
+ }
27
+
28
+ //#endregion
29
+ export { bindFill };
30
+ //# sourceMappingURL=fill-watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fill-watch.js","names":[],"sources":["../../../src/lib/fill-watch.ts"],"sourcesContent":["export function bindFill(\n\tcontrol: HTMLElement,\n\tsizeProperty: `--_sonic-${string}`,\n\tsignal: AbortSignal,\n): (isFilling: boolean) => void {\n\tlet watch: ResizeObserver | undefined;\n\n\tconst stop = (): void => {\n\t\twatch?.disconnect();\n\t\twatch = undefined;\n\t};\n\n\tsignal.addEventListener('abort', stop, { once: true });\n\n\treturn (isFilling) => {\n\t\tif (signal.aborted || isFilling === (watch !== undefined)) return;\n\n\t\tstop();\n\t\tif (!isFilling) {\n\t\t\tcontrol.style.removeProperty(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 (watch === started) {\n\t\t\t\t\tcontrol.style.setProperty(sizeProperty, `${String(box.blockSize)}px`);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\twatch = started;\n\t\tstarted.observe(control);\n\t};\n}\n"],"mappings":";AAAA,SAAgB,SACf,SACA,cACA,QAC+B;CAC/B,IAAI;CAEJ,MAAM,aAAmB;EACxB,OAAO,WAAW;EAClB,QAAQ;CACT;CAEA,OAAO,iBAAiB,SAAS,MAAM,EAAE,MAAM,KAAK,CAAC;CAErD,QAAQ,cAAc;EACrB,IAAI,OAAO,WAAW,eAAe,UAAU,SAAY;EAE3D,KAAK;EACL,IAAI,CAAC,WAAW;GACf,QAAQ,MAAM,eAAe,YAAY;GACzC;EACD;EAEA,MAAM,UAAU,IAAI,gBAAgB,YAAY;GAC/C,MAAM,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,cAAc;GAC1C,IAAI,CAAC,KAAK;GAGV,4BAA4B;IAC3B,IAAI,UAAU,SACb,QAAQ,MAAM,YAAY,cAAc,GAAG,OAAO,IAAI,SAAS,EAAE,GAAG;GAEtE,CAAC;EACF,CAAC;EAED,QAAQ;EACR,QAAQ,QAAQ,OAAO;CACxB;AACD"}
@@ -3,6 +3,11 @@ function bindHoverPreview(target, options, signal) {
3
3
  let hover;
4
4
  let isDismissed = false;
5
5
  let escapeWatch;
6
+ const hold = (value) => {
7
+ if (value === hover) return;
8
+ hover = value;
9
+ options.changed();
10
+ };
6
11
  const watchEscape = (isHovered) => {
7
12
  if (!isHovered) {
8
13
  escapeWatch?.abort();
@@ -13,14 +18,14 @@ function bindHoverPreview(target, options, signal) {
13
18
  escapeWatch = new AbortController();
14
19
  target.ownerDocument.addEventListener("keydown", (event) => {
15
20
  if (hover === void 0 || event.key !== "Escape") return;
16
- hover = void 0;
17
21
  isDismissed = true;
22
+ hold(void 0);
18
23
  options.dismiss();
19
24
  }, { signal: escapeWatch.signal });
20
25
  };
21
26
  const hoverAt = (value) => {
22
27
  if (value === void 0) isDismissed = false;
23
- if (!isDismissed && !options.isTaken()) hover = value;
28
+ if (!isDismissed && !options.isTaken()) hold(value);
24
29
  options.show();
25
30
  watchEscape(value !== void 0);
26
31
  };
@@ -36,7 +41,7 @@ function bindHoverPreview(target, options, signal) {
36
41
  }, { once: true });
37
42
  return {
38
43
  clear: () => {
39
- hover = void 0;
44
+ hold(void 0);
40
45
  },
41
46
  value: () => hover
42
47
  };
@@ -1 +1 @@
1
- {"version":3,"file":"hover-preview.js","names":[],"sources":["../../../src/lib/hover-preview.ts"],"sourcesContent":["interface HoverOptions {\n\tcanShow: () => boolean;\n\tdismiss: () => void;\n\tisTaken: () => boolean;\n\tshow: () => void;\n\tvalueAt: (event: PointerEvent) => number | undefined;\n}\n\nexport interface HoverPreview {\n\tclear: () => void;\n\tvalue: () => number | undefined;\n}\n\nexport function bindHoverPreview(\n\ttarget: HTMLElement,\n\toptions: HoverOptions,\n\tsignal: AbortSignal,\n): HoverPreview {\n\tlet hover: number | undefined;\n\tlet isDismissed = false;\n\tlet escapeWatch: AbortController | undefined;\n\n\t// A hover holds no focus, so the key is heard on the document (WCAG 1.4.13)\n\tconst watchEscape = (isHovered: boolean): void => {\n\t\tif (!isHovered) {\n\t\t\tescapeWatch?.abort();\n\t\t\tescapeWatch = undefined;\n\t\t\treturn;\n\t\t}\n\t\tif (escapeWatch) return;\n\n\t\tescapeWatch = new AbortController();\n\t\ttarget.ownerDocument.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (hover === undefined || event.key !== 'Escape') return;\n\n\t\t\t\t// Stays dismissed until the hover ends, or the next pointer move would bring it straight back\n\t\t\t\thover = undefined;\n\t\t\t\tisDismissed = true;\n\t\t\t\toptions.dismiss();\n\t\t\t},\n\t\t\t{ signal: escapeWatch.signal },\n\t\t);\n\t};\n\tconst hoverAt = (value: number | undefined): void => {\n\t\tif (value === undefined) isDismissed = false;\n\t\tif (!isDismissed && !options.isTaken()) hover = value;\n\t\toptions.show();\n\t\twatchEscape(value !== undefined);\n\t};\n\n\ttarget.addEventListener(\n\t\t'pointermove',\n\t\t(event) => {\n\t\t\tif (event.pointerType === 'touch' || event.buttons !== 0 || !options.canShow()) return;\n\n\t\t\thoverAt(options.valueAt(event));\n\t\t},\n\t\t{ signal },\n\t);\n\tfor (const type of ['pointerdown', 'pointerleave']) {\n\t\ttarget.addEventListener(\n\t\t\ttype,\n\t\t\t() => {\n\t\t\t\thoverAt(undefined);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\tsignal.addEventListener(\n\t\t'abort',\n\t\t() => {\n\t\t\twatchEscape(false);\n\t\t},\n\t\t{ once: true },\n\t);\n\n\treturn {\n\t\tclear: () => {\n\t\t\thover = undefined;\n\t\t},\n\t\tvalue: () => hover,\n\t};\n}\n"],"mappings":";AAaA,SAAgB,iBACf,QACA,SACA,QACe;CACf,IAAI;CACJ,IAAI,cAAc;CAClB,IAAI;CAGJ,MAAM,eAAe,cAA6B;EACjD,IAAI,CAAC,WAAW;GACf,aAAa,MAAM;GACnB,cAAc;GACd;EACD;EACA,IAAI,aAAa;EAEjB,cAAc,IAAI,gBAAgB;EAClC,OAAO,cAAc,iBACpB,YACC,UAAU;GACV,IAAI,UAAU,UAAa,MAAM,QAAQ,UAAU;GAGnD,QAAQ;GACR,cAAc;GACd,QAAQ,QAAQ;EACjB,GACA,EAAE,QAAQ,YAAY,OAAO,CAC9B;CACD;CACA,MAAM,WAAW,UAAoC;EACpD,IAAI,UAAU,QAAW,cAAc;EACvC,IAAI,CAAC,eAAe,CAAC,QAAQ,QAAQ,GAAG,QAAQ;EAChD,QAAQ,KAAK;EACb,YAAY,UAAU,MAAS;CAChC;CAEA,OAAO,iBACN,gBACC,UAAU;EACV,IAAI,MAAM,gBAAgB,WAAW,MAAM,YAAY,KAAK,CAAC,QAAQ,QAAQ,GAAG;EAEhF,QAAQ,QAAQ,QAAQ,KAAK,CAAC;CAC/B,GACA,EAAE,OAAO,CACV;CACA,KAAK,MAAM,QAAQ,CAAC,eAAe,cAAc,GAChD,OAAO,iBACN,YACM;EACL,QAAQ,MAAS;CAClB,GACA,EAAE,OAAO,CACV;CAED,OAAO,iBACN,eACM;EACL,YAAY,KAAK;CAClB,GACA,EAAE,MAAM,KAAK,CACd;CAEA,OAAO;EACN,aAAa;GACZ,QAAQ;EACT;EACA,aAAa;CACd;AACD"}
1
+ {"version":3,"file":"hover-preview.js","names":[],"sources":["../../../src/lib/hover-preview.ts"],"sourcesContent":["interface HoverOptions {\n\tcanShow: () => boolean;\n\tchanged: () => void;\n\tdismiss: () => void;\n\tisTaken: () => boolean;\n\tshow: () => void;\n\tvalueAt: (event: PointerEvent) => number | undefined;\n}\n\nexport interface HoverPreview {\n\tclear: () => void;\n\tvalue: () => number | undefined;\n}\n\nexport function bindHoverPreview(\n\ttarget: HTMLElement,\n\toptions: HoverOptions,\n\tsignal: AbortSignal,\n): HoverPreview {\n\tlet hover: number | undefined;\n\tlet isDismissed = false;\n\tlet escapeWatch: AbortController | undefined;\n\n\tconst hold = (value: number | undefined): void => {\n\t\tif (value === hover) return;\n\n\t\thover = value;\n\t\toptions.changed();\n\t};\n\n\t// A hover holds no focus, so the key is heard on the document (WCAG 1.4.13)\n\tconst watchEscape = (isHovered: boolean): void => {\n\t\tif (!isHovered) {\n\t\t\tescapeWatch?.abort();\n\t\t\tescapeWatch = undefined;\n\t\t\treturn;\n\t\t}\n\t\tif (escapeWatch) return;\n\n\t\tescapeWatch = new AbortController();\n\t\ttarget.ownerDocument.addEventListener(\n\t\t\t'keydown',\n\t\t\t(event) => {\n\t\t\t\tif (hover === undefined || event.key !== 'Escape') return;\n\n\t\t\t\t// Stays dismissed until the hover ends, or the next pointer move would bring it straight back\n\t\t\t\tisDismissed = true;\n\t\t\t\thold(undefined);\n\t\t\t\toptions.dismiss();\n\t\t\t},\n\t\t\t{ signal: escapeWatch.signal },\n\t\t);\n\t};\n\tconst hoverAt = (value: number | undefined): void => {\n\t\tif (value === undefined) isDismissed = false;\n\t\tif (!isDismissed && !options.isTaken()) hold(value);\n\t\toptions.show();\n\t\twatchEscape(value !== undefined);\n\t};\n\n\ttarget.addEventListener(\n\t\t'pointermove',\n\t\t(event) => {\n\t\t\tif (event.pointerType === 'touch' || event.buttons !== 0 || !options.canShow()) return;\n\n\t\t\thoverAt(options.valueAt(event));\n\t\t},\n\t\t{ signal },\n\t);\n\tfor (const type of ['pointerdown', 'pointerleave']) {\n\t\ttarget.addEventListener(\n\t\t\ttype,\n\t\t\t() => {\n\t\t\t\thoverAt(undefined);\n\t\t\t},\n\t\t\t{ signal },\n\t\t);\n\t}\n\tsignal.addEventListener(\n\t\t'abort',\n\t\t() => {\n\t\t\twatchEscape(false);\n\t\t},\n\t\t{ once: true },\n\t);\n\n\treturn {\n\t\tclear: () => {\n\t\t\thold(undefined);\n\t\t},\n\t\tvalue: () => hover,\n\t};\n}\n"],"mappings":";AAcA,SAAgB,iBACf,QACA,SACA,QACe;CACf,IAAI;CACJ,IAAI,cAAc;CAClB,IAAI;CAEJ,MAAM,QAAQ,UAAoC;EACjD,IAAI,UAAU,OAAO;EAErB,QAAQ;EACR,QAAQ,QAAQ;CACjB;CAGA,MAAM,eAAe,cAA6B;EACjD,IAAI,CAAC,WAAW;GACf,aAAa,MAAM;GACnB,cAAc;GACd;EACD;EACA,IAAI,aAAa;EAEjB,cAAc,IAAI,gBAAgB;EAClC,OAAO,cAAc,iBACpB,YACC,UAAU;GACV,IAAI,UAAU,UAAa,MAAM,QAAQ,UAAU;GAGnD,cAAc;GACd,KAAK,MAAS;GACd,QAAQ,QAAQ;EACjB,GACA,EAAE,QAAQ,YAAY,OAAO,CAC9B;CACD;CACA,MAAM,WAAW,UAAoC;EACpD,IAAI,UAAU,QAAW,cAAc;EACvC,IAAI,CAAC,eAAe,CAAC,QAAQ,QAAQ,GAAG,KAAK,KAAK;EAClD,QAAQ,KAAK;EACb,YAAY,UAAU,MAAS;CAChC;CAEA,OAAO,iBACN,gBACC,UAAU;EACV,IAAI,MAAM,gBAAgB,WAAW,MAAM,YAAY,KAAK,CAAC,QAAQ,QAAQ,GAAG;EAEhF,QAAQ,QAAQ,QAAQ,KAAK,CAAC;CAC/B,GACA,EAAE,OAAO,CACV;CACA,KAAK,MAAM,QAAQ,CAAC,eAAe,cAAc,GAChD,OAAO,iBACN,YACM;EACL,QAAQ,MAAS;CAClB,GACA,EAAE,OAAO,CACV;CAED,OAAO,iBACN,eACM;EACL,YAAY,KAAK;CAClB,GACA,EAAE,MAAM,KAAK,CACd;CAEA,OAAO;EACN,aAAa;GACZ,KAAK,MAAS;EACf;EACA,aAAa;CACd;AACD"}
@@ -11,7 +11,7 @@ function parkedPlacement(labels, index, [view, arrivingX]) {
11
11
  const parked = labels[index];
12
12
  if (!parked) return void 0;
13
13
  const next = labels[index + 1];
14
- const x = Math.max(view.insetPx, lineX(parked.start, view) + view.insetPx);
14
+ const x = Math.max(view.parkPx, lineX(parked.start, view) + view.insetPx);
15
15
  const approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;
16
16
  return {
17
17
  index,
@@ -90,11 +90,12 @@ function measureWidths(control, className, texts) {
90
90
  return widths;
91
91
  }
92
92
  function createLabelRider(control, options) {
93
- const { className, colourProperty, fadeProperty, insetProperty } = options;
93
+ const { className, colourProperty, fadeProperty, insetProperty, parkProperty } = options;
94
94
  const parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));
95
95
  const arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));
96
96
  let fadeShare = .25;
97
97
  let insetPx = 0;
98
+ let parkPx = 0;
98
99
  let measured = [];
99
100
  return {
100
101
  measure: (labels) => {
@@ -102,6 +103,7 @@ function createLabelRider(control, options) {
102
103
  const styles = getComputedStyle(control);
103
104
  fadeShare = readPxProperty(styles, fadeProperty, .25);
104
105
  insetPx = readPxProperty(styles, insetProperty, 0);
106
+ parkPx = readPxProperty(styles, parkProperty, 0);
105
107
  measured = labels.map((label, index) => ({
106
108
  ...label,
107
109
  widthPx: widths[index] ?? 0
@@ -111,7 +113,8 @@ function createLabelRider(control, options) {
111
113
  const layout = layoutRider(measured, {
112
114
  ...window,
113
115
  fadeShare,
114
- insetPx
116
+ insetPx,
117
+ parkPx
115
118
  });
116
119
  writeLabel(parked, [measured, colourProperty], layout.parked);
117
120
  writeLabel(arriving, [measured, colourProperty], layout.arriving);
@@ -1 +1 @@
1
- {"version":3,"file":"marker-rider.js","names":[],"sources":["../../../src/lib/marker-rider.ts"],"sourcesContent":["import { writeKind } from '#lib/marker-band.ts';\nimport { clamp } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild } from '#lib/render.ts';\n\ninterface RiderWindow {\n\tplayheadSeconds: number;\n\tstartSeconds: number;\n\twidthPx: number;\n\twindowSeconds: number;\n}\n\nexport interface RiderView extends RiderWindow {\n\tfadeShare: number;\n\tinsetPx: number;\n}\n\nexport interface RiderLabel {\n\tisDimmed: boolean;\n\tkind?: string;\n\tstart: number;\n\ttext: string;\n\twidthPx: number;\n}\n\ninterface LabelPlacement {\n\tindex: number;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\ninterface RiderLayout {\n\tarriving?: LabelPlacement;\n\tparked?: LabelPlacement;\n}\n\ninterface LabelSlot {\n\telement: HTMLElement;\n\tkind: string | undefined;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\nfunction lineX(seconds: number, view: RiderView): number {\n\treturn ((seconds - view.startSeconds) / view.windowSeconds) * view.widthPx;\n}\n\nfunction parkedPlacement(\n\tlabels: ReadonlyArray<RiderLabel>,\n\tindex: number,\n\t[view, arrivingX]: [RiderView, number],\n): LabelPlacement | undefined {\n\tconst parked = labels[index];\n\tif (!parked) return undefined;\n\n\tconst next = labels[index + 1];\n\tconst x = Math.max(view.insetPx, lineX(parked.start, view) + view.insetPx);\n\tconst approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;\n\n\treturn {\n\t\tindex,\n\t\topacity: view.fadeShare > 0 ? clamp(approachPx / (view.widthPx * view.fadeShare), 0, 1) : 1,\n\t\tshownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),\n\t\tx,\n\t};\n}\n\nfunction isDrawable(length: number): boolean {\n\treturn Number.isFinite(length) && length > 0;\n}\n\nexport function layoutRider(labels: ReadonlyArray<RiderLabel>, view: RiderView): RiderLayout {\n\tif (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};\n\n\tconst parkedIndex = labels.findLastIndex((label) => label.start <= view.playheadSeconds);\n\tconst next = labels[parkedIndex + 1];\n\tconst arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;\n\tconst parked = parkedPlacement(labels, parkedIndex, [view, arrivingX]);\n\tconst arriving: LabelPlacement | undefined =\n\t\tnext && arrivingX < view.widthPx\n\t\t\t? {\n\t\t\t\t\tindex: parkedIndex + 1,\n\t\t\t\t\topacity: 1,\n\t\t\t\t\tshownPx: Math.min(next.widthPx, view.widthPx - arrivingX),\n\t\t\t\t\tx: arrivingX,\n\t\t\t\t}\n\t\t\t: undefined;\n\n\treturn { ...(arriving ? { arriving } : {}), ...(parked ? { parked } : {}) };\n}\n\nfunction labelSlot(element: HTMLElement): LabelSlot {\n\treturn { element, kind: undefined, opacity: NaN, shownPx: NaN, x: NaN };\n}\n\nfunction writePlacement(\n\tslot: LabelSlot,\n\t{ opacity, shownPx, x }: LabelPlacement,\n\twidthPx: number,\n): void {\n\tconst { style } = slot.element;\n\n\tif (slot.x !== x) {\n\t\tslot.x = x;\n\t\tstyle.setProperty('translate', `${x.toFixed(2)}px`);\n\t}\n\tif (slot.shownPx !== shownPx) {\n\t\tslot.shownPx = shownPx;\n\t\tstyle.setProperty(\n\t\t\t'clip-path',\n\t\t\tshownPx < widthPx ? `inset(0 calc(100% - ${shownPx.toFixed(2)}px) 0 0)` : '',\n\t\t);\n\t}\n\tif (slot.opacity !== opacity) {\n\t\tslot.opacity = opacity;\n\t\tstyle.setProperty('opacity', opacity < 1 ? opacity.toFixed(3) : '');\n\t}\n}\n\nfunction writeLabel(\n\tslot: LabelSlot,\n\t[labels, colourProperty]: [ReadonlyArray<RiderLabel>, string],\n\tplacement: LabelPlacement | undefined,\n): void {\n\tconst { element } = slot;\n\tconst label = placement && placement.shownPx > 0 ? labels[placement.index] : undefined;\n\n\tif (element.hidden !== !label) element.hidden = !label;\n\tif (!label || !placement) return;\n\n\tif (element.textContent !== label.text) element.textContent = label.text;\n\telement.toggleAttribute('data-sonic-dimmed', label.isDimmed);\n\tif (slot.kind !== label.kind) {\n\t\tslot.kind = label.kind;\n\t\twriteKind(element, label.kind, colourProperty);\n\t}\n\twritePlacement(slot, placement, label.widthPx);\n}\n\nfunction measureWidths(\n\tcontrol: HTMLElement,\n\tclassName: string,\n\ttexts: Array<string>,\n): Array<number> {\n\tconst probes = texts.map((text) => {\n\t\tconst probe = document.createElement('div');\n\n\t\tprobe.className = className;\n\t\tprobe.textContent = text;\n\n\t\treturn probe;\n\t});\n\n\tcontrol.append(...probes);\n\n\tconst widths = probes.map((probe) => probe.getBoundingClientRect().width);\n\n\tfor (const probe of probes) probe.remove();\n\n\treturn widths;\n}\n\nexport function createLabelRider(\n\tcontrol: HTMLElement,\n\toptions: {\n\t\tclassName: string;\n\t\tcolourProperty: `--_sonic-${string}`;\n\t\tfadeProperty: `--_sonic-${string}`;\n\t\tinsetProperty: `--_sonic-${string}`;\n\t},\n): {\n\tmeasure: (labels: ReadonlyArray<Omit<RiderLabel, 'widthPx'>>) => void;\n\tplace: (window: RiderWindow) => void;\n} {\n\tconst { className, colourProperty, fadeProperty, insetProperty } = options;\n\tconst parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));\n\tconst arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));\n\tlet fadeShare = 0.25;\n\tlet insetPx = 0;\n\tlet measured: Array<RiderLabel> = [];\n\n\treturn {\n\t\tmeasure: (labels) => {\n\t\t\tconst widths = measureWidths(\n\t\t\t\tcontrol,\n\t\t\t\tclassName,\n\t\t\t\tlabels.map(({ text }) => text),\n\t\t\t);\n\n\t\t\tconst styles = getComputedStyle(control);\n\n\t\t\tfadeShare = readPxProperty(styles, fadeProperty, 0.25);\n\t\t\tinsetPx = readPxProperty(styles, insetProperty, 0);\n\t\t\tmeasured = labels\n\t\t\t\t.map((label, index) => ({ ...label, widthPx: widths[index] ?? 0 }))\n\t\t\t\t.toSorted((first, second) => first.start - second.start);\n\t\t},\n\t\tplace: (window) => {\n\t\t\tconst layout = layoutRider(measured, { ...window, fadeShare, insetPx });\n\n\t\t\twriteLabel(parked, [measured, colourProperty], layout.parked);\n\t\t\twriteLabel(arriving, [measured, colourProperty], layout.arriving);\n\t\t},\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"marker-rider.js","names":[],"sources":["../../../src/lib/marker-rider.ts"],"sourcesContent":["import { writeKind } from '#lib/marker-band.ts';\nimport { clamp } from '#lib/math.ts';\nimport { readPxProperty } from '#lib/read-px-property.ts';\nimport { requireChild } from '#lib/render.ts';\n\ninterface RiderWindow {\n\tplayheadSeconds: number;\n\tstartSeconds: number;\n\twidthPx: number;\n\twindowSeconds: number;\n}\n\nexport interface RiderView extends RiderWindow {\n\tfadeShare: number;\n\tinsetPx: number;\n\tparkPx: number;\n}\n\nexport interface RiderLabel {\n\tisDimmed: boolean;\n\tkind?: string;\n\tstart: number;\n\ttext: string;\n\twidthPx: number;\n}\n\ninterface LabelPlacement {\n\tindex: number;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\ninterface RiderLayout {\n\tarriving?: LabelPlacement;\n\tparked?: LabelPlacement;\n}\n\ninterface LabelSlot {\n\telement: HTMLElement;\n\tkind: string | undefined;\n\topacity: number;\n\tshownPx: number;\n\tx: number;\n}\n\nfunction lineX(seconds: number, view: RiderView): number {\n\treturn ((seconds - view.startSeconds) / view.windowSeconds) * view.widthPx;\n}\n\nfunction parkedPlacement(\n\tlabels: ReadonlyArray<RiderLabel>,\n\tindex: number,\n\t[view, arrivingX]: [RiderView, number],\n): LabelPlacement | undefined {\n\tconst parked = labels[index];\n\tif (!parked) return undefined;\n\n\tconst next = labels[index + 1];\n\tconst x = Math.max(view.parkPx, lineX(parked.start, view) + view.insetPx);\n\tconst approachPx = next ? lineX(next.start, view) - lineX(view.playheadSeconds, view) : Infinity;\n\n\treturn {\n\t\tindex,\n\t\topacity: view.fadeShare > 0 ? clamp(approachPx / (view.widthPx * view.fadeShare), 0, 1) : 1,\n\t\tshownPx: Math.min(parked.widthPx, arrivingX - x, view.widthPx - x),\n\t\tx,\n\t};\n}\n\nfunction isDrawable(length: number): boolean {\n\treturn Number.isFinite(length) && length > 0;\n}\n\nexport function layoutRider(labels: ReadonlyArray<RiderLabel>, view: RiderView): RiderLayout {\n\tif (!isDrawable(view.windowSeconds) || !isDrawable(view.widthPx)) return {};\n\n\tconst parkedIndex = labels.findLastIndex((label) => label.start <= view.playheadSeconds);\n\tconst next = labels[parkedIndex + 1];\n\tconst arrivingX = next ? lineX(next.start, view) + view.insetPx : Infinity;\n\tconst parked = parkedPlacement(labels, parkedIndex, [view, arrivingX]);\n\tconst arriving: LabelPlacement | undefined =\n\t\tnext && arrivingX < view.widthPx\n\t\t\t? {\n\t\t\t\t\tindex: parkedIndex + 1,\n\t\t\t\t\topacity: 1,\n\t\t\t\t\tshownPx: Math.min(next.widthPx, view.widthPx - arrivingX),\n\t\t\t\t\tx: arrivingX,\n\t\t\t\t}\n\t\t\t: undefined;\n\n\treturn { ...(arriving ? { arriving } : {}), ...(parked ? { parked } : {}) };\n}\n\nfunction labelSlot(element: HTMLElement): LabelSlot {\n\treturn { element, kind: undefined, opacity: NaN, shownPx: NaN, x: NaN };\n}\n\nfunction writePlacement(\n\tslot: LabelSlot,\n\t{ opacity, shownPx, x }: LabelPlacement,\n\twidthPx: number,\n): void {\n\tconst { style } = slot.element;\n\n\tif (slot.x !== x) {\n\t\tslot.x = x;\n\t\tstyle.setProperty('translate', `${x.toFixed(2)}px`);\n\t}\n\tif (slot.shownPx !== shownPx) {\n\t\tslot.shownPx = shownPx;\n\t\tstyle.setProperty(\n\t\t\t'clip-path',\n\t\t\tshownPx < widthPx ? `inset(0 calc(100% - ${shownPx.toFixed(2)}px) 0 0)` : '',\n\t\t);\n\t}\n\tif (slot.opacity !== opacity) {\n\t\tslot.opacity = opacity;\n\t\tstyle.setProperty('opacity', opacity < 1 ? opacity.toFixed(3) : '');\n\t}\n}\n\nfunction writeLabel(\n\tslot: LabelSlot,\n\t[labels, colourProperty]: [ReadonlyArray<RiderLabel>, string],\n\tplacement: LabelPlacement | undefined,\n): void {\n\tconst { element } = slot;\n\tconst label = placement && placement.shownPx > 0 ? labels[placement.index] : undefined;\n\n\tif (element.hidden !== !label) element.hidden = !label;\n\tif (!label || !placement) return;\n\n\tif (element.textContent !== label.text) element.textContent = label.text;\n\telement.toggleAttribute('data-sonic-dimmed', label.isDimmed);\n\tif (slot.kind !== label.kind) {\n\t\tslot.kind = label.kind;\n\t\twriteKind(element, label.kind, colourProperty);\n\t}\n\twritePlacement(slot, placement, label.widthPx);\n}\n\nfunction measureWidths(\n\tcontrol: HTMLElement,\n\tclassName: string,\n\ttexts: Array<string>,\n): Array<number> {\n\tconst probes = texts.map((text) => {\n\t\tconst probe = document.createElement('div');\n\n\t\tprobe.className = className;\n\t\tprobe.textContent = text;\n\n\t\treturn probe;\n\t});\n\n\tcontrol.append(...probes);\n\n\tconst widths = probes.map((probe) => probe.getBoundingClientRect().width);\n\n\tfor (const probe of probes) probe.remove();\n\n\treturn widths;\n}\n\nexport function createLabelRider(\n\tcontrol: HTMLElement,\n\toptions: {\n\t\tclassName: string;\n\t\tcolourProperty: `--_sonic-${string}`;\n\t\tfadeProperty: `--_sonic-${string}`;\n\t\tinsetProperty: `--_sonic-${string}`;\n\t\tparkProperty: `--_sonic-${string}`;\n\t},\n): {\n\tmeasure: (labels: ReadonlyArray<Omit<RiderLabel, 'widthPx'>>) => void;\n\tplace: (window: RiderWindow) => void;\n} {\n\tconst { className, colourProperty, fadeProperty, insetProperty, parkProperty } = options;\n\tconst parked = labelSlot(requireChild(control, `.${className}`, HTMLElement));\n\tconst arriving = labelSlot(requireChild(control, `.${className} + .${className}`, HTMLElement));\n\tlet fadeShare = 0.25;\n\tlet insetPx = 0;\n\tlet parkPx = 0;\n\tlet measured: Array<RiderLabel> = [];\n\n\treturn {\n\t\tmeasure: (labels) => {\n\t\t\tconst widths = measureWidths(\n\t\t\t\tcontrol,\n\t\t\t\tclassName,\n\t\t\t\tlabels.map(({ text }) => text),\n\t\t\t);\n\n\t\t\tconst styles = getComputedStyle(control);\n\n\t\t\tfadeShare = readPxProperty(styles, fadeProperty, 0.25);\n\t\t\tinsetPx = readPxProperty(styles, insetProperty, 0);\n\t\t\tparkPx = readPxProperty(styles, parkProperty, 0);\n\t\t\tmeasured = labels\n\t\t\t\t.map((label, index) => ({ ...label, widthPx: widths[index] ?? 0 }))\n\t\t\t\t.toSorted((first, second) => first.start - second.start);\n\t\t},\n\t\tplace: (window) => {\n\t\t\tconst layout = layoutRider(measured, { ...window, fadeShare, insetPx, parkPx });\n\n\t\t\twriteLabel(parked, [measured, colourProperty], layout.parked);\n\t\t\twriteLabel(arriving, [measured, colourProperty], layout.arriving);\n\t\t},\n\t};\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ //#region src/lib/read-ms-property.ts
2
+ function readMsProperty(styles, property, fallback) {
3
+ const text = styles.getPropertyValue(property).trim();
4
+ const parsed = Number.parseFloat(text);
5
+ if (!Number.isFinite(parsed)) return fallback;
6
+ return text.endsWith("ms") ? parsed : parsed * 1e3;
7
+ }
8
+
9
+ //#endregion
10
+ export { readMsProperty };
11
+ //# sourceMappingURL=read-ms-property.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-ms-property.js","names":[],"sources":["../../../src/lib/read-ms-property.ts"],"sourcesContent":["// Registered `<time>` properties only; engines differ on whether one computes to s or ms\nexport function readMsProperty(\n\tstyles: CSSStyleDeclaration,\n\tproperty: string,\n\tfallback: number,\n): number {\n\tconst text = styles.getPropertyValue(property).trim();\n\t// eslint-disable-next-line unicorn/prefer-number-coercion -- parsing has to stop at the unit\n\tconst parsed = Number.parseFloat(text);\n\tif (!Number.isFinite(parsed)) return fallback;\n\n\treturn text.endsWith('ms') ? parsed : parsed * 1000;\n}\n"],"mappings":";AACA,SAAgB,eACf,QACA,UACA,UACS;CACT,MAAM,OAAO,OAAO,iBAAiB,QAAQ,CAAC,CAAC,KAAK;CAEpD,MAAM,SAAS,OAAO,WAAW,IAAI;CACrC,IAAI,CAAC,OAAO,SAAS,MAAM,GAAG,OAAO;CAErC,OAAO,KAAK,SAAS,IAAI,IAAI,SAAS,SAAS;AAChD"}
@@ -106,7 +106,7 @@
106
106
  --_sonic-point-ring: var(--_sonic-lit);
107
107
  --_sonic-point-core: var(--_sonic-lit);
108
108
  --_sonic-point-lift: color-mix(in oklab, var(--_sonic-lit) 85%, #fff);
109
- --_sonic-point-edge: max(1px, var(--_sonic-point-stroke, 0px));
109
+ --_sonic-point-edge: max(1px, round(var(--_sonic-point-stroke, 0px), var(--_sonic-snap)));
110
110
  --_sonic-point-halo: transparent;
111
111
 
112
112
  z-index: 1;
@@ -103,7 +103,7 @@
103
103
  background:
104
104
  radial-gradient(
105
105
  closest-side,
106
- color-mix(in oklab, var(--_sonic-lit) calc(60% * var(--_sonic-relief)), transparent)
106
+ color-mix(in oklab, var(--_sonic-lit) calc(40% * var(--_sonic-relief)), transparent)
107
107
  calc(var(--_sonic-led-lens-ratio) * 100%),
108
108
  transparent
109
109
  ),
@@ -10,6 +10,12 @@
10
10
  initial-value: 8px;
11
11
  }
12
12
 
13
+ @property --_sonic-reveal-delay {
14
+ syntax: '<time>';
15
+ inherits: true;
16
+ initial-value: 250ms;
17
+ }
18
+
13
19
  @keyframes sonic-glass-breathe {
14
20
  to {
15
21
  opacity: calc(0.4 * min(1, var(--_sonic-glass-texture)));
@@ -47,6 +53,15 @@
47
53
  --_sonic-light-fall: calc(180deg - var(--_sonic-light-tilt));
48
54
  --_sonic-cast-x: calc(sin(var(--_sonic-light-tilt)) * var(--_sonic-unit));
49
55
  --_sonic-cast-y: calc(cos(var(--_sonic-light-tilt)) * var(--_sonic-unit));
56
+ --_sonic-snap: 1px;
57
+
58
+ @media (resolution >= 2dppx) {
59
+ --_sonic-snap: 0.5px;
60
+ }
61
+
62
+ @media (resolution >= 3dppx) {
63
+ --_sonic-snap: calc(1px / 3);
64
+ }
50
65
  }
51
66
 
52
67
  :is(
@@ -144,7 +159,8 @@
144
159
  rgb(0 0 0 / calc(0.35 * var(--_sonic-relief))),
145
160
  calc(0.5 * var(--_sonic-cast-x)) calc(0.5 * var(--_sonic-cast-y)) var(--_sonic-unit)
146
161
  rgb(255 255 255 / calc(0.12 * var(--_sonic-relief)));
147
- --_sonic-rim: 0 0 0 max(1px, 0.75 * var(--_sonic-unit))
162
+ --_sonic-rim-width: max(1px, round(0.75 * var(--_sonic-unit), var(--_sonic-snap)));
163
+ --_sonic-rim: 0 0 0 var(--_sonic-rim-width)
148
164
  var(
149
165
  --sonic-cap-edge,
150
166
  color-mix(in oklab, var(--_sonic-cap) calc(100% - 60% * var(--_sonic-relief)), #000)
@@ -167,6 +183,18 @@
167
183
  --_sonic-readout-size: var(--sonic-readout-size, 1.5rem);
168
184
  }
169
185
 
186
+ :is(
187
+ .sonic-dial,
188
+ .sonic-envelope,
189
+ .sonic-number,
190
+ .sonic-slider,
191
+ .sonic-waveform,
192
+ .sonic-wavestrip,
193
+ .sonic-xy
194
+ ) {
195
+ --_sonic-reveal-delay: var(--sonic-reveal-delay, 250ms);
196
+ }
197
+
170
198
  :is(.sonic-dial, .sonic-slider, .sonic-waveform, .sonic-wavestrip) {
171
199
  --_sonic-lit-off: var(
172
200
  --sonic-dimmed,
@@ -219,10 +247,13 @@
219
247
  );
220
248
 
221
249
  /* Invalid with the token unset, so the default takes over */
222
- --_sonic-glass-edge-set: 1px solid var(--sonic-glass-edge);
250
+ --_sonic-glass-edge-colour: var(--sonic-glass-edge);
251
+ --_sonic-glass-edge-width: var(--sonic-glass-edge-width);
252
+ --_sonic-glass-edge-set: var(--_sonic-glass-edge-width, 1px) solid
253
+ var(--_sonic-glass-edge-colour);
223
254
  --_sonic-glass-edge: var(
224
255
  --_sonic-glass-edge-set,
225
- calc(1px * (1 - var(--_sonic-relief))) solid
256
+ var(--_sonic-glass-edge-width, calc(1px * (1 - var(--_sonic-relief)))) solid
226
257
  color-mix(in oklab, currentcolor 35%, transparent)
227
258
  );
228
259
 
@@ -381,12 +412,12 @@
381
412
  --_sonic-keycap-glow: transparent;
382
413
  --_sonic-keycap-lit-glow: color-mix(
383
414
  in oklab,
384
- var(--_sonic-lit) calc(22% * var(--_sonic-relief)),
415
+ var(--_sonic-lit) calc(15% * var(--_sonic-relief)),
385
416
  transparent
386
417
  );
387
418
  --_sonic-legend-glow: drop-shadow(
388
419
  0 0 var(--_sonic-unit)
389
- color-mix(in oklab, var(--_sonic-lit) calc(35% * var(--_sonic-relief)), transparent)
420
+ color-mix(in oklab, var(--_sonic-lit) calc(24% * var(--_sonic-relief)), transparent)
390
421
  );
391
422
  }
392
423
 
@@ -409,7 +440,7 @@
409
440
  box-shadow:
410
441
  var(--_sonic-keycap-drop),
411
442
  var(--_sonic-rim),
412
- 0 0 0 max(1px, 0.75 * var(--_sonic-unit)) var(--_sonic-blank-edge, transparent),
443
+ 0 0 0 var(--_sonic-rim-width) var(--_sonic-blank-edge, transparent),
413
444
  var(--_sonic-focus-shadow);
414
445
  scale: var(--_sonic-keycap-scale);
415
446
  translate: 0 var(--_sonic-keycap-shift);
@@ -514,6 +545,8 @@
514
545
  ) {
515
546
  --_sonic-unit: calc(var(--_sonic-readout-size) / 32);
516
547
  --_sonic-glass: var(--sonic-readout-glass, var(--_sonic-glass-base));
548
+ --_sonic-glass-edge-colour: var(--sonic-readout-edge, var(--sonic-glass-edge));
549
+ --_sonic-glass-edge-width: var(--sonic-readout-edge-width, var(--sonic-glass-edge-width));
517
550
 
518
551
  box-sizing: border-box;
519
552
  place-items: center;
@@ -540,7 +573,8 @@
540
573
  box-shadow:
541
574
  calc(2 * var(--_sonic-cast-x)) calc(2 * var(--_sonic-cast-y)) calc(6 * var(--_sonic-unit))
542
575
  rgb(0 0 0 / calc(0.55 * var(--_sonic-relief))),
543
- inset 0 0 0 max(1px, var(--_sonic-unit)) rgb(255 255 255 / calc(0.08 * var(--_sonic-relief)));
576
+ inset 0 0 0 max(1px, round(var(--_sonic-unit), var(--_sonic-snap)))
577
+ rgb(255 255 255 / calc(0.08 * var(--_sonic-relief)));
544
578
  opacity: 0;
545
579
  translate: 0 calc(0.15 * var(--_sonic-readout-size));
546
580
  transition:
@@ -669,7 +703,7 @@
669
703
  --_sonic-face: ButtonFace;
670
704
  --_sonic-sheen: none;
671
705
  --_sonic-face-shadow: none;
672
- --_sonic-rim: 0 0 0 max(1px, 0.75 * var(--_sonic-unit)) var(--_sonic-ink);
706
+ --_sonic-rim: 0 0 0 var(--_sonic-rim-width) var(--_sonic-ink);
673
707
  --_sonic-drop-shade: transparent;
674
708
  --_sonic-focus-glow: 0 0 transparent;
675
709
  }
@@ -87,6 +87,11 @@
87
87
  border-radius: var(--_sonic-glass-radius);
88
88
  }
89
89
 
90
+ [fill] > .sonic-spectrum,
91
+ sonic-spectrum[fill]:not(:defined, [hidden]) {
92
+ block-size: 100%;
93
+ }
94
+
90
95
  .sonic-spectrum-canvas {
91
96
  display: block;
92
97
  inline-size: 100%;
@@ -4,6 +4,12 @@
4
4
  initial-value: 32px;
5
5
  }
6
6
 
7
+ @property --_sonic-switch-at {
8
+ syntax: '<number>';
9
+ inherits: true;
10
+ initial-value: 0;
11
+ }
12
+
7
13
  @layer sonic {
8
14
  :is(sonic-switch, .sonic-switch) {
9
15
  --_sonic-switch-size: var(--sonic-switch-size, 2rem);
@@ -52,6 +58,11 @@
52
58
  place-items: center;
53
59
  line-height: 1;
54
60
  user-select: none;
61
+ transition: --_sonic-switch-at 90ms cubic-bezier(0.4, 0, 0.2, 1);
62
+
63
+ @media (prefers-reduced-motion: reduce) {
64
+ transition: none;
65
+ }
55
66
 
56
67
  & > * {
57
68
  grid-area: 1 / 1;
@@ -243,7 +254,7 @@
243
254
  );
244
255
  border-radius: 50%;
245
256
  box-shadow:
246
- 0 0 0 max(1px, 0.75 * var(--_sonic-unit))
257
+ 0 0 0 var(--_sonic-rim-width)
247
258
  var(
248
259
  --sonic-cap-edge,
249
260
  color-mix(
@@ -257,11 +268,9 @@
257
268
  }
258
269
 
259
270
  .sonic-switch-bat {
260
- --_sonic-switch-lean: max(var(--_sonic-switch-at, 0), -1 * var(--_sonic-switch-at, 0));
261
- --_sonic-switch-transition:
262
- translate 90ms cubic-bezier(0.4, 0, 0.2, 1), inline-size 90ms cubic-bezier(0.4, 0, 0.2, 1),
263
- block-size 90ms cubic-bezier(0.4, 0, 0.2, 1),
264
- background-position 90ms cubic-bezier(0.4, 0, 0.2, 1);
271
+ --_sonic-switch-lean: max(var(--_sonic-switch-at), -1 * var(--_sonic-switch-at));
272
+ --_sonic-switch-throw: calc(var(--_sonic-switch-at) * var(--_sonic-switch-reach));
273
+ --_sonic-switch-pivot: calc((var(--_sonic-switch-size) - var(--_sonic-switch-shaft)) / 2);
265
274
  --_sonic-switch-sheen: linear-gradient(
266
275
  var(--_sonic-light-fall),
267
276
  color-mix(in oklab, var(--_sonic-switch-metal) calc(100% - 45% * var(--_sonic-relief)), #fff)
@@ -269,7 +278,7 @@
269
278
  var(--_sonic-switch-metal) 45%,
270
279
  var(--_sonic-switch-metal-shade) 88%
271
280
  );
272
- --_sonic-switch-outline: max(1px, 0.4 * var(--_sonic-unit));
281
+ --_sonic-switch-outline: max(1px, round(0.4 * var(--_sonic-unit), var(--_sonic-snap)));
273
282
  --_sonic-switch-rim: var(
274
283
  --sonic-cap-edge,
275
284
  color-mix(in oklab, var(--_sonic-switch-cap) calc(100% - 30% * var(--_sonic-relief)), #000)
@@ -286,10 +295,6 @@
286
295
  var(--_sonic-drop-shade)
287
296
  );
288
297
 
289
- @media (prefers-reduced-motion: reduce) {
290
- --_sonic-switch-transition: none;
291
- }
292
-
293
298
  &::before,
294
299
  &::after {
295
300
  position: absolute;
@@ -300,29 +305,30 @@
300
305
  background-size: var(--_sonic-switch-size) var(--_sonic-switch-size);
301
306
  border-radius: 999px;
302
307
  box-shadow: 0 0 0 var(--_sonic-switch-outline) var(--_sonic-switch-rim);
303
- transition: var(--_sonic-switch-transition);
304
308
  }
305
309
 
306
310
  &::before {
307
- --_sonic-switch-shaft-x: calc(
308
- var(--_sonic-switch-shaft) + var(--_sonic-switch-along-x) * var(--_sonic-switch-lean) *
309
- var(--_sonic-switch-reach)
311
+ --_sonic-switch-shaft-top: calc(
312
+ var(--_sonic-switch-pivot) + var(--_sonic-switch-along-y) *
313
+ min(0px, var(--_sonic-switch-throw))
310
314
  );
311
- --_sonic-switch-shaft-y: calc(
312
- var(--_sonic-switch-shaft) + var(--_sonic-switch-along-y) * var(--_sonic-switch-lean) *
313
- var(--_sonic-switch-reach)
315
+ --_sonic-switch-shaft-left: calc(
316
+ var(--_sonic-switch-pivot) + var(--_sonic-switch-along-x) *
317
+ min(0px, var(--_sonic-switch-throw))
314
318
  );
315
319
 
316
- inline-size: var(--_sonic-switch-shaft-x);
317
- block-size: var(--_sonic-switch-shaft-y);
318
- background-position: calc((var(--_sonic-switch-shaft-x) - var(--_sonic-switch-size)) / 2)
319
- calc((var(--_sonic-switch-shaft-y) - var(--_sonic-switch-size)) / 2);
320
- translate: calc(
321
- var(--_sonic-switch-along-x) * var(--_sonic-switch-at, 0) * var(--_sonic-switch-reach) / 2
320
+ inset: var(--_sonic-switch-shaft-top)
321
+ calc(
322
+ var(--_sonic-switch-pivot) - var(--_sonic-switch-along-x) *
323
+ max(0px, var(--_sonic-switch-throw))
322
324
  )
323
325
  calc(
324
- var(--_sonic-switch-along-y) * var(--_sonic-switch-at, 0) * var(--_sonic-switch-reach) / 2
325
- );
326
+ var(--_sonic-switch-pivot) - var(--_sonic-switch-along-y) *
327
+ max(0px, var(--_sonic-switch-throw))
328
+ )
329
+ var(--_sonic-switch-shaft-left);
330
+ background-position: calc(-1 * var(--_sonic-switch-shaft-left))
331
+ calc(-1 * var(--_sonic-switch-shaft-top));
326
332
  }
327
333
 
328
334
  &::after {
@@ -335,10 +341,8 @@
335
341
  rgb(0 0 0 / calc(0.3 * var(--_sonic-relief))),
336
342
  0 0 0 var(--_sonic-switch-outline) var(--_sonic-switch-rim),
337
343
  var(--_sonic-focus-shadow);
338
- translate: calc(
339
- var(--_sonic-switch-along-x) * var(--_sonic-switch-at, 0) * var(--_sonic-switch-reach)
340
- )
341
- calc(var(--_sonic-switch-along-y) * var(--_sonic-switch-at, 0) * var(--_sonic-switch-reach));
344
+ translate: calc(var(--_sonic-switch-along-x) * var(--_sonic-switch-throw))
345
+ calc(var(--_sonic-switch-along-y) * var(--_sonic-switch-throw));
342
346
  }
343
347
  }
344
348
 
@@ -347,9 +351,14 @@
347
351
  }
348
352
 
349
353
  .sonic-switch:has(> .sonic-switch-position:disabled) {
350
- --_sonic-switch-cap: var(--sonic-cap, var(--_sonic-etch));
351
- --_sonic-switch-metal: var(--_sonic-etch);
352
- --_sonic-switch-metal-shade: var(--_sonic-etch);
354
+ --_sonic-switch-disabled: color-mix(
355
+ in oklab,
356
+ var(--_sonic-ink) 30%,
357
+ oklch(from var(--_sonic-ink) clamp(0, (0.6 - l) * 1000, 1) 0 0deg)
358
+ );
359
+ --_sonic-switch-cap: var(--sonic-cap, var(--_sonic-switch-disabled));
360
+ --_sonic-switch-metal: var(--_sonic-switch-disabled);
361
+ --_sonic-switch-metal-shade: var(--_sonic-switch-disabled);
353
362
  }
354
363
 
355
364
  @media (forced-colors: active) {
@@ -16,6 +16,12 @@
16
16
  initial-value: 0;
17
17
  }
18
18
 
19
+ @property --_sonic-waveform-label-park {
20
+ syntax: '<length>';
21
+ inherits: true;
22
+ initial-value: 0;
23
+ }
24
+
19
25
  @property --_sonic-waveform-wave {
20
26
  syntax: '<color>';
21
27
  inherits: true;
@@ -81,7 +87,11 @@
81
87
  var(--_sonic-waveform-size) * var(--sonic-waveform-inset-ratio, 0.06)
82
88
  );
83
89
  --_sonic-unit: calc(var(--_sonic-waveform-size) / 48);
84
- --_sonic-waveform-line: var(--sonic-waveform-playhead-width, max(1px, var(--_sonic-unit)));
90
+ --_sonic-glass-depth: var(--sonic-glass-depth, calc(0.6 * var(--_sonic-relief)));
91
+ --_sonic-waveform-line: var(
92
+ --sonic-waveform-playhead-width,
93
+ max(1px, round(var(--_sonic-unit), var(--_sonic-snap)))
94
+ );
85
95
  --_sonic-waveform-lit: var(--_sonic-lit);
86
96
  --_sonic-waveform-wave: var(--_sonic-waveform-lit);
87
97
  --_sonic-waveform-wave-edge: var(
@@ -97,6 +107,14 @@
97
107
  --_sonic-waveform-label-inset: calc(
98
108
  var(--_sonic-waveform-size) * var(--sonic-waveform-label-inset-ratio, 0.04)
99
109
  );
110
+ --_sonic-waveform-label-lift: var(
111
+ --sonic-waveform-label-inset-block,
112
+ var(--_sonic-waveform-label-inset)
113
+ );
114
+ --_sonic-waveform-label-park: var(
115
+ --sonic-waveform-label-inset-inline,
116
+ var(--_sonic-waveform-label-inset)
117
+ );
100
118
 
101
119
  inline-size: var(--_sonic-waveform-length);
102
120
  block-size: var(--_sonic-waveform-size);
@@ -141,6 +159,13 @@
141
159
  block-size: 100%;
142
160
  }
143
161
 
162
+ .sonic-waveform-scrim {
163
+ position: absolute;
164
+ inset: var(--_sonic-waveform-inset);
165
+ pointer-events: none;
166
+ background: var(--sonic-waveform-scrim, none);
167
+ }
168
+
144
169
  :is(.sonic-waveform-playhead, .sonic-waveform-ghost) {
145
170
  position: absolute;
146
171
  inset-block: var(--_sonic-waveform-inset);
@@ -160,7 +185,7 @@
160
185
 
161
186
  .sonic-waveform-label {
162
187
  position: absolute;
163
- inset-block-end: calc(var(--_sonic-waveform-inset) + var(--_sonic-waveform-label-inset));
188
+ inset-block-end: calc(var(--_sonic-waveform-inset) + var(--_sonic-waveform-label-lift));
164
189
  inset-inline-start: var(--_sonic-waveform-inset);
165
190
  padding: 0.2em 0.35em;
166
191
  margin: -0.2em -0.35em;
@@ -199,6 +224,10 @@
199
224
  forced-color-adjust: none;
200
225
  }
201
226
 
227
+ .sonic-waveform-scrim {
228
+ background: none;
229
+ }
230
+
202
231
  .sonic-waveform-label {
203
232
  color: CanvasText;
204
233