@tremolo-ui/svelte 0.7.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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -0
  3. package/dist/actions/drag-value.d.ts +22 -0
  4. package/dist/actions/drag-value.d.ts.map +1 -0
  5. package/dist/actions/drag-value.js +30 -0
  6. package/dist/actions/drag.d.ts +14 -0
  7. package/dist/actions/drag.d.ts.map +1 -0
  8. package/dist/actions/drag.js +22 -0
  9. package/dist/actions/drop-zone.d.ts +12 -0
  10. package/dist/actions/drop-zone.d.ts.map +1 -0
  11. package/dist/actions/drop-zone.js +20 -0
  12. package/dist/actions/long-press.d.ts +11 -0
  13. package/dist/actions/long-press.d.ts.map +1 -0
  14. package/dist/actions/long-press.js +24 -0
  15. package/dist/actions/wheel.d.ts +15 -0
  16. package/dist/actions/wheel.d.ts.map +1 -0
  17. package/dist/actions/wheel.js +32 -0
  18. package/dist/components/AnimationCanvas/AnimationCanvas.svelte +69 -0
  19. package/dist/components/AnimationCanvas/AnimationCanvas.svelte.d.ts +7 -0
  20. package/dist/components/AnimationCanvas/AnimationCanvas.svelte.d.ts.map +1 -0
  21. package/dist/components/AnimationCanvas/types.d.ts +46 -0
  22. package/dist/components/AnimationCanvas/types.d.ts.map +1 -0
  23. package/dist/components/AnimationCanvas/types.js +1 -0
  24. package/dist/components/DropZone/Root.svelte +47 -0
  25. package/dist/components/DropZone/Root.svelte.d.ts +7 -0
  26. package/dist/components/DropZone/Root.svelte.d.ts.map +1 -0
  27. package/dist/components/DropZone/index.d.ts +2 -0
  28. package/dist/components/DropZone/index.d.ts.map +1 -0
  29. package/dist/components/DropZone/index.js +1 -0
  30. package/dist/components/DropZone/types.d.ts +26 -0
  31. package/dist/components/DropZone/types.d.ts.map +1 -0
  32. package/dist/components/DropZone/types.js +1 -0
  33. package/dist/components/FileInput/Root.svelte +68 -0
  34. package/dist/components/FileInput/Root.svelte.d.ts +7 -0
  35. package/dist/components/FileInput/Root.svelte.d.ts.map +1 -0
  36. package/dist/components/FileInput/Trigger.svelte +28 -0
  37. package/dist/components/FileInput/Trigger.svelte.d.ts +11 -0
  38. package/dist/components/FileInput/Trigger.svelte.d.ts.map +1 -0
  39. package/dist/components/FileInput/context.d.ts +10 -0
  40. package/dist/components/FileInput/context.d.ts.map +1 -0
  41. package/dist/components/FileInput/context.js +5 -0
  42. package/dist/components/FileInput/index.d.ts +3 -0
  43. package/dist/components/FileInput/index.d.ts.map +1 -0
  44. package/dist/components/FileInput/index.js +2 -0
  45. package/dist/components/FileInput/types.d.ts +28 -0
  46. package/dist/components/FileInput/types.d.ts.map +1 -0
  47. package/dist/components/FileInput/types.js +1 -0
  48. package/dist/components/Knob/ActiveLine.svelte +29 -0
  49. package/dist/components/Knob/ActiveLine.svelte.d.ts +6 -0
  50. package/dist/components/Knob/ActiveLine.svelte.d.ts.map +1 -0
  51. package/dist/components/Knob/InactiveLine.svelte +41 -0
  52. package/dist/components/Knob/InactiveLine.svelte.d.ts +6 -0
  53. package/dist/components/Knob/InactiveLine.svelte.d.ts.map +1 -0
  54. package/dist/components/Knob/Root.svelte +189 -0
  55. package/dist/components/Knob/Root.svelte.d.ts +10 -0
  56. package/dist/components/Knob/Root.svelte.d.ts.map +1 -0
  57. package/dist/components/Knob/SVGRoot.svelte +28 -0
  58. package/dist/components/Knob/SVGRoot.svelte.d.ts +13 -0
  59. package/dist/components/Knob/SVGRoot.svelte.d.ts.map +1 -0
  60. package/dist/components/Knob/Thumb.svelte +43 -0
  61. package/dist/components/Knob/Thumb.svelte.d.ts +7 -0
  62. package/dist/components/Knob/Thumb.svelte.d.ts.map +1 -0
  63. package/dist/components/Knob/context.d.ts +22 -0
  64. package/dist/components/Knob/context.d.ts.map +1 -0
  65. package/dist/components/Knob/context.js +5 -0
  66. package/dist/components/Knob/index.d.ts +6 -0
  67. package/dist/components/Knob/index.d.ts.map +1 -0
  68. package/dist/components/Knob/index.js +5 -0
  69. package/dist/components/Knob/types.d.ts +139 -0
  70. package/dist/components/Knob/types.d.ts.map +1 -0
  71. package/dist/components/Knob/types.js +1 -0
  72. package/dist/components/NumberInput/DecrementStepper.svelte +11 -0
  73. package/dist/components/NumberInput/DecrementStepper.svelte.d.ts +9 -0
  74. package/dist/components/NumberInput/DecrementStepper.svelte.d.ts.map +1 -0
  75. package/dist/components/NumberInput/IncrementStepper.svelte +11 -0
  76. package/dist/components/NumberInput/IncrementStepper.svelte.d.ts +9 -0
  77. package/dist/components/NumberInput/IncrementStepper.svelte.d.ts.map +1 -0
  78. package/dist/components/NumberInput/InputField.svelte +134 -0
  79. package/dist/components/NumberInput/InputField.svelte.d.ts +8 -0
  80. package/dist/components/NumberInput/InputField.svelte.d.ts.map +1 -0
  81. package/dist/components/NumberInput/Root.svelte +209 -0
  82. package/dist/components/NumberInput/Root.svelte.d.ts +10 -0
  83. package/dist/components/NumberInput/Root.svelte.d.ts.map +1 -0
  84. package/dist/components/NumberInput/Stepper.svelte +61 -0
  85. package/dist/components/NumberInput/Stepper.svelte.d.ts +10 -0
  86. package/dist/components/NumberInput/Stepper.svelte.d.ts.map +1 -0
  87. package/dist/components/NumberInput/StepperButton.svelte +63 -0
  88. package/dist/components/NumberInput/StepperButton.svelte.d.ts +10 -0
  89. package/dist/components/NumberInput/StepperButton.svelte.d.ts.map +1 -0
  90. package/dist/components/NumberInput/context.d.ts +48 -0
  91. package/dist/components/NumberInput/context.d.ts.map +1 -0
  92. package/dist/components/NumberInput/context.js +8 -0
  93. package/dist/components/NumberInput/index.d.ts +6 -0
  94. package/dist/components/NumberInput/index.d.ts.map +1 -0
  95. package/dist/components/NumberInput/index.js +5 -0
  96. package/dist/components/NumberInput/types.d.ts +104 -0
  97. package/dist/components/NumberInput/types.d.ts.map +1 -0
  98. package/dist/components/NumberInput/types.js +1 -0
  99. package/dist/components/Piano/Root.svelte +177 -0
  100. package/dist/components/Piano/Root.svelte.d.ts +10 -0
  101. package/dist/components/Piano/Root.svelte.d.ts.map +1 -0
  102. package/dist/components/Piano/index.d.ts +2 -0
  103. package/dist/components/Piano/index.d.ts.map +1 -0
  104. package/dist/components/Piano/index.js +1 -0
  105. package/dist/components/Piano/types.d.ts +90 -0
  106. package/dist/components/Piano/types.d.ts.map +1 -0
  107. package/dist/components/Piano/types.js +1 -0
  108. package/dist/components/PointsEditor/Background.svelte +14 -0
  109. package/dist/components/PointsEditor/Background.svelte.d.ts +9 -0
  110. package/dist/components/PointsEditor/Background.svelte.d.ts.map +1 -0
  111. package/dist/components/PointsEditor/Container.svelte +82 -0
  112. package/dist/components/PointsEditor/Container.svelte.d.ts +10 -0
  113. package/dist/components/PointsEditor/Container.svelte.d.ts.map +1 -0
  114. package/dist/components/PointsEditor/Point.svelte +213 -0
  115. package/dist/components/PointsEditor/Point.svelte.d.ts +7 -0
  116. package/dist/components/PointsEditor/Point.svelte.d.ts.map +1 -0
  117. package/dist/components/PointsEditor/Root.svelte +107 -0
  118. package/dist/components/PointsEditor/Root.svelte.d.ts +7 -0
  119. package/dist/components/PointsEditor/Root.svelte.d.ts.map +1 -0
  120. package/dist/components/PointsEditor/SelectionBox.svelte +32 -0
  121. package/dist/components/PointsEditor/SelectionBox.svelte.d.ts +9 -0
  122. package/dist/components/PointsEditor/SelectionBox.svelte.d.ts.map +1 -0
  123. package/dist/components/PointsEditor/context.d.ts +28 -0
  124. package/dist/components/PointsEditor/context.d.ts.map +1 -0
  125. package/dist/components/PointsEditor/context.js +5 -0
  126. package/dist/components/PointsEditor/index.d.ts +6 -0
  127. package/dist/components/PointsEditor/index.d.ts.map +1 -0
  128. package/dist/components/PointsEditor/index.js +5 -0
  129. package/dist/components/PointsEditor/types.d.ts +84 -0
  130. package/dist/components/PointsEditor/types.d.ts.map +1 -0
  131. package/dist/components/PointsEditor/types.js +1 -0
  132. package/dist/components/Slider/Marks.svelte +42 -0
  133. package/dist/components/Slider/Marks.svelte.d.ts +7 -0
  134. package/dist/components/Slider/Marks.svelte.d.ts.map +1 -0
  135. package/dist/components/Slider/MarksOption.svelte +63 -0
  136. package/dist/components/Slider/MarksOption.svelte.d.ts +7 -0
  137. package/dist/components/Slider/MarksOption.svelte.d.ts.map +1 -0
  138. package/dist/components/Slider/Root.svelte +217 -0
  139. package/dist/components/Slider/Root.svelte.d.ts +10 -0
  140. package/dist/components/Slider/Root.svelte.d.ts.map +1 -0
  141. package/dist/components/Slider/Thumb.svelte +84 -0
  142. package/dist/components/Slider/Thumb.svelte.d.ts +10 -0
  143. package/dist/components/Slider/Thumb.svelte.d.ts.map +1 -0
  144. package/dist/components/Slider/Track.svelte +51 -0
  145. package/dist/components/Slider/Track.svelte.d.ts +7 -0
  146. package/dist/components/Slider/Track.svelte.d.ts.map +1 -0
  147. package/dist/components/Slider/context.d.ts +28 -0
  148. package/dist/components/Slider/context.d.ts.map +1 -0
  149. package/dist/components/Slider/context.js +5 -0
  150. package/dist/components/Slider/index.d.ts +6 -0
  151. package/dist/components/Slider/index.d.ts.map +1 -0
  152. package/dist/components/Slider/index.js +5 -0
  153. package/dist/components/Slider/types.d.ts +138 -0
  154. package/dist/components/Slider/types.d.ts.map +1 -0
  155. package/dist/components/Slider/types.js +1 -0
  156. package/dist/components/XYPad/Area.svelte +29 -0
  157. package/dist/components/XYPad/Area.svelte.d.ts +10 -0
  158. package/dist/components/XYPad/Area.svelte.d.ts.map +1 -0
  159. package/dist/components/XYPad/Root.svelte +238 -0
  160. package/dist/components/XYPad/Root.svelte.d.ts +10 -0
  161. package/dist/components/XYPad/Root.svelte.d.ts.map +1 -0
  162. package/dist/components/XYPad/Thumb.svelte +109 -0
  163. package/dist/components/XYPad/Thumb.svelte.d.ts +10 -0
  164. package/dist/components/XYPad/Thumb.svelte.d.ts.map +1 -0
  165. package/dist/components/XYPad/context.d.ts +25 -0
  166. package/dist/components/XYPad/context.d.ts.map +1 -0
  167. package/dist/components/XYPad/context.js +5 -0
  168. package/dist/components/XYPad/index.d.ts +4 -0
  169. package/dist/components/XYPad/index.d.ts.map +1 -0
  170. package/dist/components/XYPad/index.js +3 -0
  171. package/dist/components/XYPad/types.d.ts +80 -0
  172. package/dist/components/XYPad/types.d.ts.map +1 -0
  173. package/dist/components/XYPad/types.js +1 -0
  174. package/dist/components/_util/VisuallyHiddenRangeInput.svelte +17 -0
  175. package/dist/components/_util/VisuallyHiddenRangeInput.svelte.d.ts +8 -0
  176. package/dist/components/_util/VisuallyHiddenRangeInput.svelte.d.ts.map +1 -0
  177. package/dist/components/_util/check-steps.svelte.d.ts +10 -0
  178. package/dist/components/_util/check-steps.svelte.d.ts.map +1 -0
  179. package/dist/components/_util/check-steps.svelte.js +17 -0
  180. package/dist/components/_util/placement.d.ts +15 -0
  181. package/dist/components/_util/placement.d.ts.map +1 -0
  182. package/dist/components/_util/placement.js +30 -0
  183. package/dist/components/_util/style.d.ts +5 -0
  184. package/dist/components/_util/style.d.ts.map +1 -0
  185. package/dist/components/_util/style.js +9 -0
  186. package/dist/hooks/useMIDIAccess.svelte.d.ts +29 -0
  187. package/dist/hooks/useMIDIAccess.svelte.d.ts.map +1 -0
  188. package/dist/hooks/useMIDIAccess.svelte.js +62 -0
  189. package/dist/hooks/useMIDIInput.svelte.d.ts +18 -0
  190. package/dist/hooks/useMIDIInput.svelte.d.ts.map +1 -0
  191. package/dist/hooks/useMIDIInput.svelte.js +31 -0
  192. package/dist/hooks/useMIDIMessage.svelte.d.ts +9 -0
  193. package/dist/hooks/useMIDIMessage.svelte.d.ts.map +1 -0
  194. package/dist/hooks/useMIDIMessage.svelte.js +23 -0
  195. package/dist/index.d.ts +33 -0
  196. package/dist/index.d.ts.map +1 -0
  197. package/dist/index.js +26 -0
  198. package/package.json +72 -0
@@ -0,0 +1,104 @@
1
+ import type { InputEventOption, ModifierValue } from '@tremolo-ui/dom';
2
+ import type { Scale } from '@tremolo-ui/functions';
3
+ import type { Snippet } from 'svelte';
4
+ export interface NumberInputProps {
5
+ /** The current value. Bind it (`bind:value`) or update it from `onChange`. */
6
+ value: number;
7
+ /** The lowest value. Leave it out for no lower end. */
8
+ min?: number;
9
+ /** The highest value. Leave it out for no upper end. */
10
+ max?: number;
11
+ /**
12
+ * Granularity of the value. The wheel, the arrow keys and the steppers
13
+ * snap it to multiples of `step`.
14
+ * @default 1
15
+ */
16
+ step?: number;
17
+ /**
18
+ * How the value is distributed between `min` and `max`, for a
19
+ * `normalized` amount.
20
+ * @default linearScale
21
+ */
22
+ scale?: Scale;
23
+ /**
24
+ * The text shown for a value.
25
+ * @default String
26
+ */
27
+ format?: (value: number) => string;
28
+ /**
29
+ * Read a value back out of the text. Has to undo `format`. Text with no
30
+ * number in it reads as `NaN`, which leaves the value alone.
31
+ * @default parseNumberText
32
+ */
33
+ parse?: (text: string) => number;
34
+ /**
35
+ * Keep the value within `min` and `max` when it is committed or stepped.
36
+ * Typing is never clamped.
37
+ * @default true
38
+ */
39
+ clampValue?: boolean;
40
+ /**
41
+ * How much one notch of the wheel moves the value, while the focus is
42
+ * inside. `null` turns the wheel off.
43
+ * @default ['raw', 1]
44
+ */
45
+ wheel?: ModifierValue<InputEventOption> | null;
46
+ /**
47
+ * How much one arrow key press moves the value. `null` turns the arrow
48
+ * keys off.
49
+ * @default { default: ['raw', 1], shift: ['raw', 0.1] }
50
+ */
51
+ keyboard?: ModifierValue<InputEventOption> | null;
52
+ /**
53
+ * Pixels of vertical drag on `Stepper` that move the value by one `step`.
54
+ * `null` turns the drag off.
55
+ * @default 1
56
+ */
57
+ drag?: number | null;
58
+ /**
59
+ * How much a `Stepper` drag counts, per modifier key.
60
+ * @default { default: 1, shift: 0.1 }
61
+ */
62
+ dragSensitivity?: ModifierValue<number>;
63
+ /**
64
+ * Hide the cursor while dragging a `Stepper`.
65
+ * @default false
66
+ */
67
+ pointerLock?: boolean;
68
+ /**
69
+ * Select the text when `InputField` takes focus: all of it, only the
70
+ * number without its unit, or nothing.
71
+ * @default 'none'
72
+ */
73
+ selectOnFocus?: 'all' | 'number' | 'none';
74
+ /**
75
+ * Show the plain value while `InputField` has focus, dropping whatever
76
+ * `format` put around it.
77
+ * @default false
78
+ */
79
+ unformatOnFocus?: boolean;
80
+ /**
81
+ * Put the caret back at the same digit after an arrow key steps the value.
82
+ * @default false
83
+ */
84
+ keepCaretOnStep?: boolean;
85
+ /**
86
+ * Commit and leave the field on Enter.
87
+ * @default true
88
+ */
89
+ blurOnEnter?: boolean;
90
+ /** Make the input unchangeable and remove it from the tab order. */
91
+ disabled?: boolean;
92
+ /** Make the value unchangeable while leaving the field focusable. */
93
+ readonly?: boolean;
94
+ /**
95
+ * Called with the new value: as it is typed (unclamped), and when it is
96
+ * committed or stepped.
97
+ */
98
+ onChange?: (value: number) => void;
99
+ /** The root element, bound with `bind:ref`. */
100
+ ref?: HTMLDivElement | null;
101
+ /** The input renders exactly what you compose here. */
102
+ children: Snippet;
103
+ }
104
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/NumberInput/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACtE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAA;AAElD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAErC,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAA;IACb,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,KAAK,CAAA;IACb;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAClC;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IAChC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAA;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAA;IACjD;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;IACzC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,+CAA+C;IAC/C,GAAG,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAC3B,uDAAuD;IACvD,QAAQ,EAAE,OAAO,CAAA;CAClB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,177 @@
1
+ <script lang="ts">
2
+ import {
3
+ blackKeyWidth,
4
+ createPianoInput,
5
+ fitWhiteKeyWidth,
6
+ getNoteRangeArray,
7
+ notePosition,
8
+ pianoWidth,
9
+ type PianoInputInstance,
10
+ type PianoLayout,
11
+ } from '@tremolo-ui/dom'
12
+ import { isWhiteKey, noteKey } from '@tremolo-ui/functions'
13
+ import { untrack } from 'svelte'
14
+
15
+ import type { KeyState, PianoProps } from './types.js'
16
+
17
+ import type { HTMLAttributes } from 'svelte/elements'
18
+
19
+ type Props = PianoProps &
20
+ Omit<HTMLAttributes<HTMLDivElement>, keyof PianoProps>
21
+
22
+ let {
23
+ noteRange,
24
+ glissando = true,
25
+ midiMax = 127,
26
+ keyboardShortcuts,
27
+ keyboardShortcutsScope = 'root',
28
+ resizable = false,
29
+ whiteKeyWidth = 40,
30
+ keyGap = 1,
31
+ blackKeyWidthRatio = 0.65,
32
+ blackKeyHeightRatio = 0.6,
33
+ classes,
34
+ label,
35
+ keyProps,
36
+ onPlayNote,
37
+ onStopNote,
38
+ ref = $bindable(null),
39
+ style,
40
+ ...rest
41
+ }: Props = $props()
42
+
43
+ let activeNotes: number[] = $state([])
44
+ /** Set while `resizable` is on, measured from the parent. */
45
+ let resizedKeyWidth: number | null = $state(null)
46
+
47
+ const notes = $derived(getNoteRangeArray(noteRange))
48
+ const layout: PianoLayout = $derived({
49
+ noteRange,
50
+ whiteKeyWidth: resizable
51
+ ? (resizedKeyWidth ?? whiteKeyWidth)
52
+ : whiteKeyWidth,
53
+ keyGap,
54
+ blackKeyWidthRatio,
55
+ blackKeyHeightRatio,
56
+ })
57
+
58
+ let instance: PianoInputInstance | null = null
59
+
60
+ // Only the element decides how the instance is wired. Everything else is
61
+ // pushed with update() below, so that changing the layout mid-drag — the
62
+ // parent being resized under `resizable`, say — does not abort the drag.
63
+ $effect(() => {
64
+ if (!ref) return
65
+ const current = createPianoInput(
66
+ ref,
67
+ untrack(() => ({
68
+ layout,
69
+ glissando,
70
+ midiMax,
71
+ keyboardShortcuts,
72
+ keyboardShortcutsScope,
73
+ onPlayNote: (note, velocity) => onPlayNote?.(note, velocity),
74
+ onStopNote: (note) => onStopNote?.(note),
75
+ onActiveNotesChange: (next) => {
76
+ activeNotes = next
77
+ },
78
+ })),
79
+ )
80
+ instance = current
81
+ return () => {
82
+ instance = null
83
+ current.destroy()
84
+ }
85
+ })
86
+
87
+ $effect(() => {
88
+ instance?.update({
89
+ layout,
90
+ glissando,
91
+ midiMax,
92
+ keyboardShortcuts,
93
+ keyboardShortcutsScope,
94
+ })
95
+ })
96
+
97
+ $effect(() => {
98
+ if (!resizable || !ref) return
99
+ const node = ref
100
+ const parent = node.parentElement
101
+ if (!parent) throw new Error("doesn't have a parent element.")
102
+ const fit = {
103
+ noteRange: { first: noteRange.first, last: noteRange.last },
104
+ keyGap,
105
+ blackKeyWidthRatio,
106
+ }
107
+ const observer = new ResizeObserver(() => {
108
+ resizedKeyWidth = fitWhiteKeyWidth(node.clientWidth, fit)
109
+ })
110
+ observer.observe(parent)
111
+ return () => observer.disconnect()
112
+ })
113
+
114
+ /** Start a note, as a key would. */
115
+ export function playNote(note: number, velocity?: number) {
116
+ instance?.noteOn(note, { source: 'api', velocity })
117
+ }
118
+
119
+ /** Release a note started with `playNote`. */
120
+ export function stopNote(note: number) {
121
+ instance?.noteOff(note, { source: 'api' })
122
+ }
123
+
124
+ function keyState(note: number, index: number): KeyState {
125
+ return {
126
+ index,
127
+ keyType: isWhiteKey(note) ? 'white' : 'black',
128
+ active: activeNotes.includes(note),
129
+ disabled: note > midiMax,
130
+ }
131
+ }
132
+ </script>
133
+
134
+ <!-- The group can own keyboard shortcuts and must receive focus. The width
135
+ is computed from the layout rather than chosen, so it stays inline. -->
136
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
137
+ <div
138
+ bind:this={ref}
139
+ role="group"
140
+ tabindex="0"
141
+ data-resizable={resizable ? '' : undefined}
142
+ style="position: relative; width: {resizable
143
+ ? '100%'
144
+ : `${pianoWidth(layout)}px`}; {style ?? ''}"
145
+ {...rest}
146
+ >
147
+ {#each notes as note, index (note)}
148
+ {@const state = keyState(note, index)}
149
+ {@const { style: keyStyle, ...attributes } = keyProps?.(note, state) ?? {}}
150
+ <!-- Placed and sized from the layout, which needs it out of flow; the
151
+ black keys sit over the white ones whichever order they are drawn in.
152
+ The geometry comes after the caller's style. -->
153
+ <div
154
+ data-note={note}
155
+ data-note-key={noteKey(note)}
156
+ data-active={state.active ? '' : undefined}
157
+ data-disabled={state.disabled ? '' : undefined}
158
+ {...attributes}
159
+ style="position: absolute; z-index: {state.keyType === 'white'
160
+ ? 1
161
+ : 2}; {keyStyle ?? ''}"
162
+ style:left="{notePosition(note, layout)}px"
163
+ style:width="{state.keyType === 'white'
164
+ ? layout.whiteKeyWidth
165
+ : blackKeyWidth(layout)}px"
166
+ style:height={state.keyType === 'white'
167
+ ? '100%'
168
+ : `${blackKeyHeightRatio * 100}%`}
169
+ >
170
+ {#if label}
171
+ <div class={classes?.keyLabelWrapper}>
172
+ <div class={classes?.keyLabel}>{@render label(note, state)}</div>
173
+ </div>
174
+ {/if}
175
+ </div>
176
+ {/each}
177
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { PianoProps } from './types.js';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Props = PianoProps & Omit<HTMLAttributes<HTMLDivElement>, keyof PianoProps>;
4
+ declare const Root: import("svelte").Component<Props, {
5
+ playNote: (note: number, velocity?: number) => void;
6
+ stopNote: (note: number) => void;
7
+ }, "ref">;
8
+ type Root = ReturnType<typeof Root>;
9
+ export default Root;
10
+ //# sourceMappingURL=Root.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Root.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/Piano/Root.svelte.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAY,UAAU,EAAE,MAAM,YAAY,CAAA;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,KAAK,GAAG,UAAU,GACrB,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,MAAM,UAAU,CAAC,CAAC;AAsJ3D,QAAA,MAAM,IAAI;qBA9CiB,MAAM,aAAa,MAAM;qBAKzB,MAAM;SAyCiB,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { default as Root } from './Root.svelte';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Piano/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAA"}
@@ -0,0 +1 @@
1
+ export { default as Root } from './Root.svelte';
@@ -0,0 +1,90 @@
1
+ import type { KeyboardShortcuts, KeyboardShortcutsScope, NoteRange } from '@tremolo-ui/dom';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ /** What a key is, when `label` or `keyProps` is asked about it. */
5
+ export interface KeyState {
6
+ /** Position in the note range, counting from `noteRange.first`. */
7
+ index: number;
8
+ keyType: 'white' | 'black';
9
+ /** Whether the note is currently sounding. */
10
+ active: boolean;
11
+ /** Whether the note is above `midiMax` and cannot sound. */
12
+ disabled: boolean;
13
+ }
14
+ /** What `keyProps` may return for one key. */
15
+ export type KeyAttributes = HTMLAttributes<HTMLDivElement> & Record<`data-${string}`, string | number | boolean | undefined>;
16
+ export interface PianoProps {
17
+ /** Classes for the label inside each key. */
18
+ classes?: {
19
+ keyLabelWrapper?: string;
20
+ keyLabel?: string;
21
+ };
22
+ /** The notes drawn, from `first` to `last`. */
23
+ noteRange: NoteRange;
24
+ /**
25
+ * Let a pointer slide from one key to the next while it is down.
26
+ * @default true
27
+ */
28
+ glissando?: boolean;
29
+ /**
30
+ * Highest note that can sound. Keys above it carry `data-disabled`.
31
+ * @default 127
32
+ */
33
+ midiMax?: number;
34
+ /**
35
+ * Play notes from the computer keyboard: `keys[i]` plays
36
+ * `noteRange.first + i`. `SHORTCUTS` from `@tremolo-ui/dom` has
37
+ * ready-made layouts.
38
+ */
39
+ keyboardShortcuts?: KeyboardShortcuts;
40
+ /**
41
+ * Where keyboard shortcuts listen: while the piano has focus (`root`), or
42
+ * anywhere on the page except in editable elements (`window`).
43
+ * @default 'root'
44
+ */
45
+ keyboardShortcutsScope?: KeyboardShortcutsScope;
46
+ /**
47
+ * Follow the width of the parent element, deriving the width of a white
48
+ * key from it. `whiteKeyWidth` is ignored.
49
+ * @default false
50
+ */
51
+ resizable?: boolean;
52
+ /**
53
+ * Width of a white key in pixels, not counting `keyGap`.
54
+ * @default 40
55
+ */
56
+ whiteKeyWidth?: number;
57
+ /**
58
+ * Space between two white keys, in pixels.
59
+ * @default 1
60
+ */
61
+ keyGap?: number;
62
+ /**
63
+ * Width of a black key, as a fraction of `whiteKeyWidth`.
64
+ * @default 0.65
65
+ */
66
+ blackKeyWidthRatio?: number;
67
+ /**
68
+ * Height of a black key, as a fraction of the height of the keyboard.
69
+ * @default 0.6
70
+ */
71
+ blackKeyHeightRatio?: number;
72
+ /**
73
+ * What to draw inside a key, as a snippet taking the note and its state.
74
+ * Draw nothing to leave the key bare.
75
+ */
76
+ label?: Snippet<[number, KeyState]>;
77
+ /**
78
+ * Extra attributes for one key, by note: a class, a style, a `data-*`
79
+ * attribute to select on. The geometry of the key is applied after the
80
+ * returned style and cannot be overridden.
81
+ */
82
+ keyProps?: (note: number, state: KeyState) => KeyAttributes;
83
+ /** Called when a note starts sounding. */
84
+ onPlayNote?: (note: number, velocity?: number) => void;
85
+ /** Called once everything holding a note has let go of it. */
86
+ onStopNote?: (note: number) => void;
87
+ /** The root element, bound with `bind:ref`. */
88
+ ref?: HTMLDivElement | null;
89
+ }
90
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/Piano/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,sBAAsB,EACtB,SAAS,EACV,MAAM,iBAAiB,CAAA;AAExB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,mEAAmE;AACnE,MAAM,WAAW,QAAQ;IACvB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,GAAG,OAAO,CAAA;IAC1B,8CAA8C;IAC9C,MAAM,EAAE,OAAO,CAAA;IACf,4DAA4D;IAC5D,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,8CAA8C;AAC9C,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC,cAAc,CAAC,GACxD,MAAM,CAAC,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;AAEjE,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACzD,+CAA+C;IAC/C,SAAS,EAAE,SAAS,CAAA;IACpB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,sBAAsB,CAAA;IAC/C;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAA;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,KAAK,aAAa,CAAA;IAC3D,0CAA0C;IAC1C,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACtD,8DAA8D;IAC9D,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,+CAA+C;IAC/C,GAAG,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;CAC5B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { HTMLAttributes } from 'svelte/elements'
4
+
5
+ type Props = HTMLAttributes<HTMLDivElement> & { children?: Snippet }
6
+
7
+ let { children, style, ...rest }: Props = $props()
8
+ </script>
9
+
10
+ <!-- Behind the points, filling the editor. The order of the layers holds
11
+ whichever way they are written, so the z-index is part of the mechanics. -->
12
+ <div style="position: absolute; inset: 0; z-index: 0; {style ?? ''}" {...rest}>
13
+ {@render children?.()}
14
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Props = HTMLAttributes<HTMLDivElement> & {
4
+ children?: Snippet;
5
+ };
6
+ declare const Background: import("svelte").Component<Props, {}, "">;
7
+ type Background = ReturnType<typeof Background>;
8
+ export default Background;
9
+ //# sourceMappingURL=Background.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Background.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/PointsEditor/Background.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,KAAK,GAAG,cAAc,CAAC,cAAc,CAAC,GAAG;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAgBvE,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,82 @@
1
+ <script lang="ts">
2
+ import {
3
+ createDragValue,
4
+ elementMapping,
5
+ POINT_AXIS,
6
+ wheelMove,
7
+ } from '@tremolo-ui/dom'
8
+
9
+ import { wheel as wheelAction } from '../../actions/wheel.js'
10
+ import { setPlacement } from '../_util/placement.js'
11
+
12
+ import { usePointsEditorContext } from './context.js'
13
+
14
+ import type { Snippet } from 'svelte'
15
+ import type { HTMLAttributes } from 'svelte/elements'
16
+
17
+ type Props = HTMLAttributes<HTMLDivElement> & {
18
+ ref?: HTMLDivElement | null
19
+ children?: Snippet
20
+ }
21
+
22
+ let { ref = $bindable(null), children, style, ...rest }: Props = $props()
23
+
24
+ const points = usePointsEditorContext()
25
+ setPlacement('PointsEditor.Container')
26
+
27
+ // The container is what the pointer position is normalized against.
28
+ $effect(() => {
29
+ points.setContainer(ref)
30
+ return () => points.setContainer(null)
31
+ })
32
+
33
+ // A drag on empty space draws a selection box. It is only attached while
34
+ // there is a selection to draw: `createDrag` puts `touch-action: none` on
35
+ // what it holds, and an editor that cannot select should not stop the page
36
+ // scrolling under a finger.
37
+ const selecting = $derived(points.selectable && !points.disabled)
38
+
39
+ $effect(() => {
40
+ const element = ref
41
+ if (!element || !selecting) return
42
+ const drag = createDragValue(element, {
43
+ axis: POINT_AXIS,
44
+ mapping: elementMapping(() => element),
45
+ // A press that landed on a point belongs to that point. Declining here
46
+ // rather than later matters: by then the container would already have
47
+ // taken the pointer capture away from the point.
48
+ shouldStart: (event) =>
49
+ !points.editor.isPointElement(event.target as Element | null),
50
+ onDragStart: ([x, y], state) =>
51
+ points.editor.beginSelectionBox({ x, y }, state.event),
52
+ onChange: ([x, y]) => points.editor.moveSelectionBox({ x, y }),
53
+ onDragEnd: () => points.editor.endSelectionBox(),
54
+ })
55
+ return () => drag.destroy()
56
+ })
57
+
58
+ // One listener for the whole editor rather than one per point: a wheel
59
+ // event only reaches what the cursor is over, and a point is a small target.
60
+ const wheelOptions = {
61
+ onWheel: (event: WheelEvent) => {
62
+ // Scrolling up moves the point towards y = 0; shift switches to x.
63
+ const move = wheelMove(event)
64
+ if (!move) return
65
+ const axis = move.axis === 0 ? 'x' : 'y'
66
+ if (points.editor.nudgeFocusedPoint(axis, move.direction, event)) {
67
+ event.preventDefault()
68
+ }
69
+ },
70
+ }
71
+ </script>
72
+
73
+ <!-- The points inside are placed against this box, over the background and
74
+ under the selection box. -->
75
+ <div
76
+ bind:this={ref}
77
+ style="position: absolute; inset: 0; z-index: 10; {style ?? ''}"
78
+ use:wheelAction={wheelOptions}
79
+ {...rest}
80
+ >
81
+ {@render children?.()}
82
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Props = HTMLAttributes<HTMLDivElement> & {
4
+ ref?: HTMLDivElement | null;
5
+ children?: Snippet;
6
+ };
7
+ declare const Container: import("svelte").Component<Props, {}, "ref">;
8
+ type Container = ReturnType<typeof Container>;
9
+ export default Container;
10
+ //# sourceMappingURL=Container.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Container.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/PointsEditor/Container.svelte.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,KAAK,GAAG,cAAc,CAAC,cAAc,CAAC,GAAG;IAC5C,GAAG,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAC;AAuEJ,QAAA,MAAM,SAAS,8CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}