zds-pickers 4.1.4 → 4.1.5

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.
@@ -16,32 +16,30 @@ const baseNotes = [
16
16
  'A#',
17
17
  'B',
18
18
  ];
19
- // const { midiToFreq, midiToNoteName } = Midi
20
- // console.log(Note.names(), Note.midi('C4'))
21
- // const firstNote = 21
22
- // const lastNote = 108
23
19
  const numNotes = 12;
24
20
  const whiteNotes = numNotes * 0.7; // approx
25
21
  const audioContext = new window.AudioContext();
26
- const BaseOctavePlayer = ({ octave, height, width, ...rest }) => {
22
+ const BaseOctavePlayer = ({ className, octave, height, renderNoteLabel, width, ...rest }) => {
27
23
  const keyWidth = width / whiteNotes + 2;
28
24
  const keyWidthToHeight = keyWidth / height;
29
25
  const firstNote = Note.midi(baseNotes[0] + String(octave)) || 0;
30
26
  const lastNote = Note.midi(baseNotes[baseNotes.length - 1] + String(octave)) || 0;
31
- return (_jsx("div", { children: _jsx(ReactPiano, { keyWidthToHeight: keyWidthToHeight, noteRange: { first: firstNote, last: lastNote }, width: width, ...rest }) }));
27
+ return (_jsx("div", { children: _jsx(ReactPiano, { className: className, keyWidthToHeight: keyWidthToHeight, noteRange: { first: firstNote, last: lastNote }, renderNoteLabel: renderNoteLabel, width: width, ...rest }) }));
32
28
  };
33
- const WithProvider = ({ format, height, hostname, instrumentName, octave = 4, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, selectedNotes, soundfont, Tooltip, width, }) => (_jsx(SoundfontProvider, { audioContext,
29
+ const WithProvider = ({ className, format, height, hostname, instrumentName, octave = 4, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, renderNoteLabel, selectedNotes, soundfont, Tooltip, width, }) => (_jsx(SoundfontProvider, { audioContext,
34
30
  format,
35
31
  hostname,
36
32
  instrumentName,
37
33
  onChange,
38
- soundfont, render: ({ isLoading, playNote, stopNote }) => (_jsx(BaseOctavePlayer, { disabled: isLoading, height,
34
+ soundfont, render: ({ isLoading, playNote, stopNote }) => (_jsx(BaseOctavePlayer, { disabled: isLoading, className,
35
+ height,
39
36
  octave,
40
37
  onClick,
41
38
  onDoubleClick,
42
39
  onKeyMouseEnter,
43
40
  onKeyMouseLeave,
44
41
  playNote,
42
+ renderNoteLabel,
45
43
  selectedNotes,
46
44
  stopNote,
47
45
  Tooltip,
@@ -9,91 +9,104 @@ if (typeof window !== 'undefined') {
9
9
  window.MIDI = { Soundfont: {} };
10
10
  }
11
11
  }
12
- // Also ensure it's available globally (not just on window)
13
- if (typeof globalThis !== 'undefined') {
14
- if (!globalThis.MIDI) {
15
- globalThis.MIDI = window.MIDI || { Soundfont: {} };
16
- }
17
- }
18
- // Also set it on the global object for Node.js compatibility
19
- if (typeof global !== 'undefined') {
20
- if (!global.MIDI) {
21
- global.MIDI = window.MIDI || { Soundfont: {} };
22
- }
12
+ // Also ensure it's on globalThis (browser sync + Node/SSR; avoid bare `global`)
13
+ if (typeof globalThis !== 'undefined' && !globalThis.MIDI) {
14
+ globalThis.MIDI =
15
+ typeof window !== 'undefined' && window.MIDI
16
+ ? window.MIDI
17
+ : { Soundfont: {} };
23
18
  }
24
19
  // CRITICAL: Import at top level to ensure bundler processes it
25
- // Import bundled soundfont to avoid CSP issues in Electron apps
20
+ // Import bundled soundfont to avoid CSP issues in Electron apps
26
21
  // NOTE: This adds ~2.2MB to the bundle size. Currently all consumers need piano,
27
22
  // but if future web apps don't need audio, consider making this import conditional
28
23
  // or implementing lazy loading to reduce bundle size for audio-free use cases.
29
24
  import '../soundfonts/acoustic_grand_piano-mp3';
30
25
  const isDefined = (item) => item !== undefined && item !== null && !Number.isNaN(item);
26
+ const midiToNoteName = (midiNumber) => {
27
+ const num = typeof midiNumber === 'string'
28
+ ? Number.parseInt(midiNumber, 10)
29
+ : midiNumber;
30
+ return Note.fromMidi(num);
31
+ };
32
+ const createBundledPlayer = (soundfontData) => {
33
+ const player = {
34
+ play: (note, velocity) => {
35
+ const noteName = midiToNoteName(note);
36
+ const noteData = soundfontData[noteName];
37
+ if (!noteData) {
38
+ return { stop: () => { } };
39
+ }
40
+ const audio = new Audio(noteData);
41
+ audio.volume = (velocity ?? 127) / 127;
42
+ audio.play();
43
+ return {
44
+ stop: () => {
45
+ audio.pause();
46
+ audio.currentTime = 0;
47
+ },
48
+ };
49
+ },
50
+ stop: () => [],
51
+ };
52
+ return player;
53
+ };
54
+ const loadBundledOrRemoteInstrument = (audioContext, instrumentName, format, soundfont, hostname) => {
55
+ const soundfontData = window.MIDI?.Soundfont?.[instrumentName];
56
+ if (soundfontData) {
57
+ return Promise.resolve(createBundledPlayer(soundfontData));
58
+ }
59
+ // biome-ignore lint/suspicious/noConsole: intentional diagnostics for missing bundled soundfont
60
+ console.warn(`[zds-pickers] Bundled soundfont "${instrumentName}" not found; falling back to remote load.`);
61
+ if (isDefined(hostname)) {
62
+ return Soundfont.instrument(audioContext, instrumentName, {
63
+ format,
64
+ soundfont,
65
+ nameToUrl: (name, sf, fmt) => `${hostname}/${sf}/${name}-${fmt}.js`,
66
+ });
67
+ }
68
+ return Soundfont.instrument(audioContext, instrumentName, {
69
+ format,
70
+ soundfont,
71
+ });
72
+ };
31
73
  const SoundfontProvider = (props) => {
32
74
  const { audioContext, format, hostname, instrumentName, onChange, render, soundfont, } = props;
33
75
  const [isLoading, setIsLoading] = useState(true);
34
76
  const [activeAudioNodes, setActiveAudioNodes] = useState({});
35
77
  const [instrument, setInstrument] = useState(null);
36
78
  const loadInstrument = useCallback((nm) => {
37
- // Re-trigger loading state
38
79
  setInstrument(null);
80
+ setIsLoading(true);
81
+ if (!instrumentName) {
82
+ setIsLoading(false);
83
+ return;
84
+ }
39
85
  if (isDefined(hostname)) {
40
- // pull from an url
41
86
  Soundfont.instrument(audioContext, nm, {
42
87
  format,
43
88
  soundfont,
44
89
  nameToUrl: (name, sf, fmt) => `${hostname}/${sf}/${name}-${fmt}.js`,
45
- }).then((newInstrument) => {
90
+ })
91
+ .then(newInstrument => {
46
92
  setInstrument(newInstrument);
47
- });
48
- }
49
- else if (instrumentName) {
50
- // the host app has already injected the soundfont into global space
51
- // Try to create a player directly from our bundled soundfont data
52
- const soundfontData = window.MIDI?.Soundfont?.[instrumentName];
53
- if (soundfontData) {
54
- // MIDI note number to note name conversion using Tonal
55
- const midiToNoteName = (midiNumber) => {
56
- const num = typeof midiNumber === 'string'
57
- ? Number.parseInt(midiNumber, 10)
58
- : midiNumber;
59
- return Note.fromMidi(num);
60
- };
61
- // Create a simple player that can play notes from our bundled data
62
- const player = {
63
- play: (note, velocity) => {
64
- // Convert MIDI note number to note name if needed
65
- const noteName = midiToNoteName(note);
66
- const noteData = soundfontData[noteName];
67
- if (noteData) {
68
- // Create audio from the data URL
69
- const audio = new Audio(noteData);
70
- audio.volume = (velocity || 127) / 127;
71
- audio.play();
72
- // Return a compatible object with stop method
73
- return {
74
- stop: () => {
75
- audio.pause();
76
- audio.currentTime = 0;
77
- },
78
- };
79
- }
80
- return null;
81
- },
82
- stop: () => {
83
- // Simple stop implementation
84
- },
85
- };
86
- setInstrument(player);
93
+ })
94
+ .catch(error => {
95
+ // biome-ignore lint/suspicious/noConsole: intentional diagnostics for load failures
96
+ console.error(`[zds-pickers] Failed to load soundfont "${nm}" from ${hostname}:`, error);
87
97
  setIsLoading(false);
88
- }
89
- else {
90
- setIsLoading(false);
91
- }
98
+ });
99
+ return;
92
100
  }
93
- else {
94
- // sound must be disabled
101
+ loadBundledOrRemoteInstrument(audioContext, nm, format, soundfont, hostname)
102
+ .then(newInstrument => {
103
+ setInstrument(newInstrument);
104
+ })
105
+ .catch(error => {
106
+ // biome-ignore lint/suspicious/noConsole: intentional diagnostics for load failures
107
+ console.error(`[zds-pickers] Failed to load soundfont "${nm}":`, error);
95
108
  setIsLoading(false);
96
- }
109
+ });
97
110
  }, [audioContext, format, hostname, instrumentName, soundfont]);
98
111
  useEffect(() => {
99
112
  if (instrumentName) {
@@ -109,7 +122,7 @@ const SoundfontProvider = (props) => {
109
122
  if (instrument) {
110
123
  audioContext.resume().then(() => {
111
124
  const audioNode = instrument.play(String(midiNumber));
112
- setActiveAudioNodes((prev) => ({ ...prev, [midiNumber]: audioNode }));
125
+ setActiveAudioNodes(prev => ({ ...prev, [midiNumber]: audioNode }));
113
126
  });
114
127
  }
115
128
  };
@@ -120,7 +133,7 @@ const SoundfontProvider = (props) => {
120
133
  }
121
134
  const audioNode = activeAudioNodes[midiNumber];
122
135
  audioNode.stop();
123
- setActiveAudioNodes((prev) => {
136
+ setActiveAudioNodes(prev => {
124
137
  const { [midiNumber]: _, ...rest } = prev;
125
138
  return rest;
126
139
  });
@@ -136,7 +149,7 @@ const SoundfontProvider = (props) => {
136
149
  node.stop();
137
150
  }
138
151
  }
139
- setActiveAudioNodes(nodes);
152
+ setActiveAudioNodes({});
140
153
  });
141
154
  };
142
155
  return render({
@@ -0,0 +1 @@
1
+ export {};
@@ -1,9 +1,27 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import classNames from 'classnames';
2
3
  import { useCallback } from 'react';
4
+ import { Note } from 'tonal';
3
5
  import { OctavePlayer } from '../other/OctavePlayer';
4
6
  import { noSelection } from './Select';
7
+ const formatPitchName = (pitchClass) => pitchClass.replace(/([A-G])b/g, '$1\u266D');
8
+ const NATURAL_LABEL_NUDGE_LEFT = new Set(['C', 'F']);
9
+ const NATURAL_LABEL_NUDGE_RIGHT = new Set(['E', 'B']);
10
+ const noteNameLabelRenderer = ({ isAccidental, isActive, midiNumber, }) => {
11
+ const pitchClass = Note.pitchClass(Note.fromMidi(midiNumber) ?? '');
12
+ const pitchName = formatPitchName(pitchClass);
13
+ if (!pitchName)
14
+ return null;
15
+ return (_jsx("div", { className: classNames('ReactPiano__NoteLabel', 'ReactPiano__NoteLabel--noteName', {
16
+ 'ReactPiano__NoteLabel--active': isActive,
17
+ 'ReactPiano__NoteLabel--accidental': isAccidental,
18
+ 'ReactPiano__NoteLabel--natural': !isAccidental,
19
+ 'ReactPiano__NoteLabel--nudgeLeft': !isAccidental && NATURAL_LABEL_NUDGE_LEFT.has(pitchClass),
20
+ 'ReactPiano__NoteLabel--nudgeRight': !isAccidental && NATURAL_LABEL_NUDGE_RIGHT.has(pitchClass),
21
+ }), children: pitchName }));
22
+ };
5
23
  const KeyPicker = (props) => {
6
- const { value, onChange, disabled = false, height = 100, width = 300, octave = 4, ...rest } = props;
24
+ const { value, onChange, disabled = false, height = 100, showNoteNames = false, width = 300, octave = 4, ...rest } = props;
7
25
  const handleKeyClick = useCallback((note) => {
8
26
  if (disabled)
9
27
  return;
@@ -14,6 +32,6 @@ const KeyPicker = (props) => {
14
32
  const octaveStart = 60 + (octave - 4) * 12;
15
33
  const octaveEnd = octaveStart + 11;
16
34
  const shouldHighlight = value !== noSelection && value >= octaveStart && value <= octaveEnd;
17
- return (_jsx(OctavePlayer, { ...rest, selectedNotes: shouldHighlight ? [value] : [], disabled: disabled, height: height, width: width, octave: octave, onClick: handleKeyClick, instrumentName: "acoustic_grand_piano" }));
35
+ return (_jsx(OctavePlayer, { ...rest, className: showNoteNames ? 'ReactPiano--showNoteNames' : undefined, selectedNotes: shouldHighlight ? [value] : [], disabled: disabled, height: height, renderNoteLabel: showNoteNames ? noteNameLabelRenderer : undefined, width: width, octave: octave, onClick: handleKeyClick, instrumentName: "acoustic_grand_piano" }));
18
36
  };
19
37
  export { KeyPicker };
@@ -1,34 +1,6 @@
1
- import type { PianoProps } from 'lib/pickers/PianoPicker';
2
- import type Soundfont from 'soundfont-player';
3
- import type { TooltipProps } from './DefaultTooltip';
4
- type PianoMidiNote = {
5
- accidental: boolean;
6
- left: number;
7
- midiNumber: number;
8
- width: number;
9
- };
10
- type OctavePlayerProps = PianoProps & {
11
- disabled?: boolean;
12
- height: number;
13
- octave: number;
14
- width: number;
15
- };
16
- type WithProviderProps = PianoProps & {
17
- format?: 'mp3' | 'ogg';
18
- height: number;
19
- hostname?: string;
20
- instrumentName?: Soundfont.InstrumentName;
21
- octave?: number;
22
- onChange?: (note: number) => void;
23
- onClick?: (note: number) => void;
24
- onDoubleClick?: (note: number) => void;
25
- onKeyMouseEnter?: (note: PianoMidiNote) => void;
26
- onKeyMouseLeave?: (note: number) => void;
27
- selectedNotes?: number[];
28
- soundfont?: 'MusyngKite' | 'FluidR3_GM';
29
- Tooltip?: TooltipProps;
30
- width: number;
31
- };
32
- declare const WithProvider: ({ format, height, hostname, instrumentName, octave, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, selectedNotes, soundfont, Tooltip, width, }: WithProviderProps) => import("react/jsx-runtime").JSX.Element;
1
+ import type { OctavePlayerProps, PianoMidiNote, PianoProviderProps } from './pianoTypes';
2
+ declare const WithProvider: ({ className, format, height, hostname, instrumentName, octave, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, renderNoteLabel, selectedNotes, soundfont, Tooltip, width, }: PianoProviderProps) => import("react/jsx-runtime").JSX.Element;
33
3
  export { WithProvider as OctavePlayer };
34
- export type { OctavePlayerProps, PianoMidiNote, WithProviderProps };
4
+ export type { OctavePlayerProps, PianoMidiNote, PianoProviderProps };
5
+ /** @deprecated Use PianoProviderProps */
6
+ export type WithProviderProps = PianoProviderProps;
@@ -0,0 +1,40 @@
1
+ import type { ReactNode } from 'react';
2
+ import type Soundfont from 'soundfont-player';
3
+ import type { TooltipProps } from './DefaultTooltip';
4
+ type NoteLabelRenderProps = {
5
+ isAccidental: boolean;
6
+ isActive: boolean;
7
+ keyboardShortcut?: string | null;
8
+ midiNumber: number;
9
+ };
10
+ type PianoMidiNote = {
11
+ accidental: boolean;
12
+ left: number;
13
+ midiNumber: number;
14
+ width: number;
15
+ };
16
+ type PianoProps = {
17
+ className?: string;
18
+ disabled?: boolean;
19
+ height: number;
20
+ renderNoteLabel?: (props: NoteLabelRenderProps) => ReactNode;
21
+ selectedNotes?: number[];
22
+ width: number;
23
+ };
24
+ type PianoProviderProps = PianoProps & {
25
+ format?: 'mp3' | 'ogg';
26
+ hostname?: string;
27
+ instrumentName?: Soundfont.InstrumentName;
28
+ octave?: number;
29
+ onChange?: (note: number) => void;
30
+ onClick?: (note: number) => void;
31
+ onDoubleClick?: (note: number) => void;
32
+ onKeyMouseEnter?: (note: PianoMidiNote) => void;
33
+ onKeyMouseLeave?: (note: number) => void;
34
+ soundfont?: 'MusyngKite' | 'FluidR3_GM';
35
+ Tooltip?: TooltipProps;
36
+ };
37
+ type OctavePlayerProps = PianoProps & {
38
+ octave: number;
39
+ };
40
+ export type { NoteLabelRenderProps, OctavePlayerProps, PianoMidiNote, PianoProps, PianoProviderProps, };
@@ -1,9 +1,10 @@
1
1
  import { type OctavePlayerProps } from '../other/OctavePlayer';
2
- type KeyPickerProps = Omit<OctavePlayerProps, 'selectedNotes' | 'onClick'> & {
2
+ type KeyPickerProps = Omit<OctavePlayerProps, 'className' | 'onClick' | 'renderNoteLabel' | 'selectedNotes'> & {
3
3
  value: number;
4
4
  onChange: (value: number) => void;
5
5
  disabled?: boolean;
6
6
  height?: number;
7
+ showNoteNames?: boolean;
7
8
  width?: number;
8
9
  octave?: number;
9
10
  };
@@ -1,11 +1,4 @@
1
- import type { WithProviderProps } from '../other/OctavePlayer';
2
- type PianoProps = {
3
- activeNotes?: number[];
4
- selectedNotes?: number[];
5
- disabled: boolean;
6
- height: number;
7
- width: number;
8
- };
9
- declare const WithProvider: ({ format, height, hostname, instrumentName, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, selectedNotes, soundfont, width, }: WithProviderProps) => import("react/jsx-runtime").JSX.Element;
1
+ import type { PianoProps, PianoProviderProps } from '../other/pianoTypes';
2
+ declare const WithProvider: ({ format, height, hostname, instrumentName, onChange, onClick, onDoubleClick, onKeyMouseEnter, onKeyMouseLeave, selectedNotes, soundfont, width, }: PianoProviderProps) => import("react/jsx-runtime").JSX.Element;
10
3
  export { WithProvider as PianoPicker };
11
4
  export type { PianoProps };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zds-pickers",
3
- "version": "4.1.4",
3
+ "version": "4.1.5",
4
4
  "main": "./dist/index.cjs.js",
5
5
  "module": "./dist/index.es.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -16,14 +16,8 @@
16
16
  "access": "public",
17
17
  "registry": "https://registry.npmjs.org/"
18
18
  },
19
- "sideEffects": [
20
- "lib/soundfonts/*.js",
21
- "dist/soundfonts/*.js"
22
- ],
23
- "files": [
24
- "dist",
25
- "lib/soundfonts"
26
- ],
19
+ "sideEffects": ["lib/soundfonts/*.js", "dist/soundfonts/*.js"],
20
+ "files": ["dist", "lib/soundfonts"],
27
21
  "homepage": "https://github.com/dkadrios/zds-pickers#readme",
28
22
  "license": "MIT",
29
23
  "author": "Darin Kadrioski <dkadrios@gmail.com>",
@@ -37,13 +31,12 @@
37
31
  },
38
32
  "devDependencies": {
39
33
  "@biomejs/biome": "1.9.4",
40
- "@storybook/addon-actions": "8.6.3",
41
- "@storybook/addon-docs": "8.6.3",
42
- "@storybook/addon-essentials": "8.6.3",
43
- "@storybook/addon-links": "8.6.3",
44
- "@storybook/builder-vite": "8.6.3",
45
- "@storybook/react": "8.6.3",
46
- "@storybook/react-vite": "8.6.3",
34
+ "@storybook/addon-docs": "8.6.18",
35
+ "@storybook/addon-essentials": "8.6.18",
36
+ "@storybook/addon-links": "8.6.18",
37
+ "@storybook/builder-vite": "8.6.18",
38
+ "@storybook/react": "8.6.18",
39
+ "@storybook/react-vite": "8.6.18",
47
40
  "@types/d3-drag": "3.0.7",
48
41
  "@types/d3-scale": "4.0.9",
49
42
  "@types/d3-selection": "3.0.11",
@@ -56,7 +49,10 @@
56
49
  "nodemon": "3.1.9",
57
50
  "prop-types": "15.8.1",
58
51
  "rc-slider": "11.1.8",
59
- "storybook": "8.6.3",
52
+ "react": "18.3.1",
53
+ "react-dom": "18.3.1",
54
+ "react-select": "5.10.1",
55
+ "storybook": "8.6.18",
60
56
  "typescript": "5.8.2",
61
57
  "vite": "^6.3.6",
62
58
  "zds-mappings": "1.4.9"