zds-pickers 4.2.1 → 4.4.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.
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export * from './utils';
2
2
  export * from './midi/ccValues';
3
3
  export * from './midi/export';
4
4
  export * from './other/DefaultTooltip';
5
+ export * from './other/noteNames';
5
6
  export * from './other/OctavePlayer';
6
7
  export * from './other/SvgText';
7
8
  export * from './pickers/CCPicker';
@@ -0,0 +1,22 @@
1
+ import { Midi, Note } from 'tonal';
2
+ const { midiToNoteName } = Midi;
3
+ /**
4
+ * Rewrites ASCII accidentals in a note name to their Unicode glyphs, so
5
+ * labels read as `D♭` / `F♯` rather than `Db` / `F#`.
6
+ */
7
+ const formatPitchName = (name) => name.replace(/([A-G])b/g, '$1♭').replace(/([A-G])#/g, '$1♯');
8
+ /**
9
+ * Pitch class (no octave) for a MIDI note, as a display label.
10
+ *
11
+ * `noteLabels`, when supplied, is a chroma-indexed array of names — index 0 is
12
+ * C, index 1 is C♯/D♭, and so on. It lets a caller impose key-aware spelling
13
+ * (a scale may legitimately need both B♭ and F♯), which a single sharps/flats
14
+ * flag cannot express. Missing entries fall back to the default flat spelling.
15
+ */
16
+ const pitchClassLabel = (midiNumber, noteLabels) => noteLabels?.[((midiNumber % 12) + 12) % 12] ??
17
+ formatPitchName(Note.pitchClass(Note.fromMidi(midiNumber) ?? ''));
18
+ /**
19
+ * Note name with octave for a MIDI note, flats by default (`70` → `B♭4`).
20
+ */
21
+ const midiNoteLabel = (note) => formatPitchName(midiToNoteName(note, { sharps: false }));
22
+ export { formatPitchName, midiNoteLabel, pitchClassLabel };
@@ -1,15 +1,18 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import classNames from 'classnames';
3
- import { useCallback } from 'react';
3
+ import { useCallback, useMemo } from 'react';
4
4
  import { Note } from 'tonal';
5
5
  import { OctavePlayer } from '../other/OctavePlayer';
6
+ import { pitchClassLabel } from '../other/noteNames';
6
7
  import { noSelection } from './Select';
7
- const formatPitchName = (pitchClass) => pitchClass.replace(/([A-G])b/g, '$1\u266D');
8
8
  const NATURAL_LABEL_NUDGE_LEFT = new Set(['C', 'F']);
9
9
  const NATURAL_LABEL_NUDGE_RIGHT = new Set(['E', 'B']);
10
- const noteNameLabelRenderer = ({ isAccidental, isActive, midiNumber, }) => {
10
+ const makeNoteNameLabelRenderer = (noteLabels) => ({ isAccidental, isActive, midiNumber }) => {
11
+ // Nudge classes key off the natural letter, which never varies with the
12
+ // caller's spelling choice, so they read the default name rather than the
13
+ // (possibly overridden) label.
11
14
  const pitchClass = Note.pitchClass(Note.fromMidi(midiNumber) ?? '');
12
- const pitchName = formatPitchName(pitchClass);
15
+ const pitchName = pitchClassLabel(midiNumber, noteLabels);
13
16
  if (!pitchName)
14
17
  return null;
15
18
  return (_jsx("div", { className: classNames('ReactPiano__NoteLabel', 'ReactPiano__NoteLabel--noteName', {
@@ -21,7 +24,8 @@ const noteNameLabelRenderer = ({ isAccidental, isActive, midiNumber, }) => {
21
24
  }), children: pitchName }));
22
25
  };
23
26
  const KeyPicker = (props) => {
24
- const { value, onChange, disabled = false, height = 100, showNoteNames = false, width = 300, octave = 4, ...rest } = props;
27
+ const { value, onChange, disabled = false, height = 100, noteLabels, showNoteNames = false, width = 300, octave = 4, ...rest } = props;
28
+ const renderNoteLabel = useMemo(() => makeNoteNameLabelRenderer(noteLabels), [noteLabels]);
25
29
  const handleKeyClick = useCallback((note) => {
26
30
  if (disabled)
27
31
  return;
@@ -32,6 +36,6 @@ const KeyPicker = (props) => {
32
36
  const octaveStart = 60 + (octave - 4) * 12;
33
37
  const octaveEnd = octaveStart + 11;
34
38
  const shouldHighlight = value !== noSelection && value >= octaveStart && value <= octaveEnd;
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" }));
39
+ return (_jsx(OctavePlayer, { ...rest, className: showNoteNames ? 'ReactPiano--showNoteNames' : undefined, selectedNotes: shouldHighlight ? [value] : [], disabled: disabled, height: height, renderNoteLabel: showNoteNames ? renderNoteLabel : undefined, width: width, octave: octave, onClick: handleKeyClick, instrumentName: "acoustic_grand_piano" }));
36
40
  };
37
41
  export { KeyPicker };
@@ -1,11 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef, useCallback, useMemo } from 'react';
3
- import { Midi } from 'tonal';
4
3
  import { emptyMapping } from 'zds-mappings';
5
4
  import useStateWithDynamicDefault from '../hooks/useStateWithDynamicDefault';
5
+ import { midiNoteLabel } from '../other/noteNames';
6
6
  import { assertRange } from '../utils';
7
7
  import { Select } from './Select';
8
- const { midiToNoteName } = Midi;
9
8
  const formattedMapEntry = ({ note, name }) => `${note} ${name.length ? '-' : ''} ${name}`;
10
9
  const formattedListEntry = (label, idx) => ({
11
10
  label,
@@ -16,12 +15,7 @@ const NotePicker = forwardRef((props, ref) => {
16
15
  const options = useMemo(() => {
17
16
  if (isMelodicMode) {
18
17
  return emptyMapping()
19
- .map(({ note }) => {
20
- const midiNoteName = midiToNoteName(note, { sharps: false })
21
- .replace('b', '♭')
22
- .replace('#', '♯');
23
- return `${midiNoteName} (#${note})`;
24
- })
18
+ .map(({ note }) => `${midiNoteLabel(note)} (#${note})`)
25
19
  .map(formattedListEntry);
26
20
  }
27
21
  return (mapping || emptyMapping())
@@ -2,6 +2,7 @@ export * from './utils';
2
2
  export * from './midi/ccValues';
3
3
  export * from './midi/export';
4
4
  export * from './other/DefaultTooltip';
5
+ export * from './other/noteNames';
5
6
  export * from './other/OctavePlayer';
6
7
  export * from './other/SvgText';
7
8
  export * from './pickers/CCPicker';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Rewrites ASCII accidentals in a note name to their Unicode glyphs, so
3
+ * labels read as `D♭` / `F♯` rather than `Db` / `F#`.
4
+ */
5
+ declare const formatPitchName: (name: string) => string;
6
+ /**
7
+ * Pitch class (no octave) for a MIDI note, as a display label.
8
+ *
9
+ * `noteLabels`, when supplied, is a chroma-indexed array of names — index 0 is
10
+ * C, index 1 is C♯/D♭, and so on. It lets a caller impose key-aware spelling
11
+ * (a scale may legitimately need both B♭ and F♯), which a single sharps/flats
12
+ * flag cannot express. Missing entries fall back to the default flat spelling.
13
+ */
14
+ declare const pitchClassLabel: (midiNumber: number, noteLabels?: readonly string[]) => string;
15
+ /**
16
+ * Note name with octave for a MIDI note, flats by default (`70` → `B♭4`).
17
+ */
18
+ declare const midiNoteLabel: (note: number) => string;
19
+ export { formatPitchName, midiNoteLabel, pitchClassLabel };
@@ -4,6 +4,12 @@ type KeyPickerProps = Omit<PianoProviderProps, 'className' | 'instrumentName' |
4
4
  onChange: (value: number) => void;
5
5
  disabled?: boolean;
6
6
  height?: number;
7
+ /**
8
+ * Chroma-indexed note names (index 0 = C) overriding the default flat
9
+ * spelling — lets a caller label keys for a specific key/scale, where both
10
+ * sharps and flats can be correct at once. Missing entries fall back.
11
+ */
12
+ noteLabels?: readonly string[];
7
13
  showNoteNames?: boolean;
8
14
  width?: number;
9
15
  octave?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zds-pickers",
3
- "version": "4.2.1",
3
+ "version": "4.4.0",
4
4
  "main": "./dist/index.cjs.js",
5
5
  "module": "./dist/index.es.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -55,6 +55,7 @@
55
55
  "react-dom": "18.3.1",
56
56
  "react-select": "5.10.1",
57
57
  "storybook": "10.5.5",
58
+ "tonal": "6.4.3",
58
59
  "typescript": "5.8.2",
59
60
  "vite": "^8.0.16",
60
61
  "vitest": "4.1.10",
@@ -66,7 +67,6 @@
66
67
  "d3-selection": "3.0.0",
67
68
  "react-svgmt": "2.0.2",
68
69
  "soundfont-player": "0.12.0",
69
- "tonal": "6.4.2",
70
70
  "zds-react-piano": "4.1.0"
71
71
  },
72
72
  "peerDependencies": {
@@ -74,6 +74,7 @@
74
74
  "react": ">=18.0.0",
75
75
  "react-dom": ">=18.0.0",
76
76
  "react-select": ">=5.10.0",
77
+ "tonal": ">=6.4.2",
77
78
  "zds-mappings": "^1.4.9"
78
79
  },
79
80
  "scripts": {