zds-pickers 4.1.0 → 4.1.3
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.cjs.js +9 -9
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +56 -57
- package/dist/index.es.js.map +1 -1
- package/dist/other/SoundFontProvider.js +5 -4
- package/dist/pickers/KeyPicker.js +1 -2
- package/dist/types/other/SoundFontProvider.d.ts +1 -1
- package/dist/types/pickers/KeyPicker.d.ts +1 -4
- package/lib/soundfonts/acoustic_grand_piano-mp3.js +107 -0
- package/package.json +5 -1
- package/.editorconfig +0 -19
- package/.github/workflows/codeql-analysis.yml +0 -67
- package/.nvmrc +0 -1
- package/.prettierignore +0 -0
- package/.prettierrc +0 -13
- package/.storybook/main.ts +0 -42
- package/.storybook/manager.ts +0 -12
- package/.storybook/preview.tsx +0 -26
- package/.vscode/extensions.json +0 -32
- package/.vscode/javascript.code-snippets +0 -229
- package/.vscode/settings.json +0 -358
- package/biome.jsonc +0 -158
- package/lib/@types/nebiru-react-rotary-knob.d.ts +0 -20
- package/lib/@types/react-svgmt.d.ts +0 -74
- package/lib/@types/zds-mappings.d.ts +0 -55
- package/lib/@types/zds-react-piano.d.ts +0 -17
- package/lib/hooks/useComponentSize.ts +0 -52
- package/lib/hooks/useStateWithDynamicDefault.ts +0 -13
- package/lib/index.ts +0 -52
- package/lib/midi/ccValues.ts +0 -139
- package/lib/midi/export.ts +0 -88
- package/lib/other/DefaultTooltip.ts +0 -14
- package/lib/other/OctavePlayer.tsx +0 -139
- package/lib/other/SoundFontProvider.tsx +0 -213
- package/lib/other/SvgText.tsx +0 -221
- package/lib/pickers/CCPicker.tsx +0 -28
- package/lib/pickers/ChannelMappingPicker.tsx +0 -150
- package/lib/pickers/ChannelPicker.tsx +0 -84
- package/lib/pickers/KeyPicker.tsx +0 -56
- package/lib/pickers/Knob.tsx +0 -93
- package/lib/pickers/KnobPicker.tsx +0 -93
- package/lib/pickers/LatchPicker.tsx +0 -33
- package/lib/pickers/MappingPicker.tsx +0 -68
- package/lib/pickers/NotePicker.tsx +0 -95
- package/lib/pickers/PianoPicker.tsx +0 -81
- package/lib/pickers/PolarityPicker.tsx +0 -39
- package/lib/pickers/ResponseCurve.tsx +0 -251
- package/lib/pickers/ResponseCurvePicker.tsx +0 -75
- package/lib/pickers/Select.tsx +0 -268
- package/lib/pickers/StatusPicker.tsx +0 -30
- package/lib/pickers/ValuePicker.tsx +0 -67
- package/lib/pickers/knobSkin10.tsx +0 -112
- package/lib/rotaryKnob/InternalInput.tsx +0 -43
- package/lib/rotaryKnob/Types.ts +0 -6
- package/lib/rotaryKnob/helpers/DrawCircle.tsx +0 -41
- package/lib/rotaryKnob/helpers/DrawLine.tsx +0 -32
- package/lib/rotaryKnob/helpers/HelpersOverlay.tsx +0 -43
- package/lib/rotaryKnob/helpers/KnobVisualHelpers.tsx +0 -138
- package/lib/rotaryKnob/index.tsx +0 -417
- package/lib/rotaryKnob/knobdefaultskin.ts +0 -69
- package/lib/rotaryKnob/utils.ts +0 -112
- package/lib/utils.ts +0 -41
- package/stories/assets/sampleStyles.css +0 -365
- package/stories/assets/utils.js +0 -7
- package/stories/picker.stories.tsx +0 -432
- package/tsconfig.json +0 -35
- package/vite.config.ts +0 -41
- /package/{lib/temp → dist/soundfonts}/acoustic_grand_piano-mp3.js +0 -0
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
// See https://github.com/danigb/soundfont-player
|
|
2
|
-
// for more documentation on prop options.
|
|
3
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
-
import Soundfont from 'soundfont-player'
|
|
5
|
-
import { Note } from 'tonal'
|
|
6
|
-
|
|
7
|
-
// Type for the MIDI global object
|
|
8
|
-
interface MIDIGlobal {
|
|
9
|
-
Soundfont: Record<string, Record<string, string>>
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Extend global interfaces to include MIDI
|
|
13
|
-
declare global {
|
|
14
|
-
interface Window {
|
|
15
|
-
MIDI?: MIDIGlobal
|
|
16
|
-
}
|
|
17
|
-
var MIDI: MIDIGlobal | undefined
|
|
18
|
-
namespace globalThis {
|
|
19
|
-
var MIDI: MIDIGlobal | undefined
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
// Ensure MIDI global exists before importing soundfont
|
|
23
|
-
if (typeof window !== 'undefined') {
|
|
24
|
-
if (!window.MIDI) {
|
|
25
|
-
window.MIDI = { Soundfont: {} }
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Also ensure it's available globally (not just on window)
|
|
30
|
-
if (typeof globalThis !== 'undefined') {
|
|
31
|
-
if (!globalThis.MIDI) {
|
|
32
|
-
globalThis.MIDI = window.MIDI || { Soundfont: {} }
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Also set it on the global object for Node.js compatibility
|
|
37
|
-
if (typeof global !== 'undefined') {
|
|
38
|
-
if (!global.MIDI) {
|
|
39
|
-
global.MIDI = window.MIDI || { Soundfont: {} }
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Import bundled soundfont to avoid CSP issues in Electron apps
|
|
44
|
-
// NOTE: This adds ~2.2MB to the bundle size. Currently all consumers need piano,
|
|
45
|
-
// but if future web apps don't need audio, consider making this import conditional
|
|
46
|
-
// or implementing lazy loading to reduce bundle size for audio-free use cases.
|
|
47
|
-
import '../temp/acoustic_grand_piano-mp3'
|
|
48
|
-
|
|
49
|
-
const isDefined = <T,>(item: T) =>
|
|
50
|
-
item !== undefined && item !== null && !Number.isNaN(item)
|
|
51
|
-
|
|
52
|
-
type SoundfontProviderProps = {
|
|
53
|
-
audioContext: AudioContext
|
|
54
|
-
format?: 'mp3' | 'ogg'
|
|
55
|
-
hostname?: string
|
|
56
|
-
instrumentName?: Soundfont.InstrumentName
|
|
57
|
-
onChange?: (midiNumber: number) => void
|
|
58
|
-
render: (props: {
|
|
59
|
-
isLoading: boolean
|
|
60
|
-
playNote: (midiNumber: number) => void
|
|
61
|
-
stopNote: (midiNumber: number) => void
|
|
62
|
-
stopAllNotes: () => void
|
|
63
|
-
}) => React.ReactNode
|
|
64
|
-
soundfont?: 'MusyngKite' | 'FluidR3_GM'
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const SoundfontProvider = (props: SoundfontProviderProps) => {
|
|
68
|
-
const {
|
|
69
|
-
audioContext,
|
|
70
|
-
format,
|
|
71
|
-
hostname,
|
|
72
|
-
instrumentName,
|
|
73
|
-
onChange,
|
|
74
|
-
render,
|
|
75
|
-
soundfont,
|
|
76
|
-
} = props
|
|
77
|
-
|
|
78
|
-
const [isLoading, setIsLoading] = useState(true)
|
|
79
|
-
const [activeAudioNodes, setActiveAudioNodes] = useState<
|
|
80
|
-
Record<number, Soundfont.Player>
|
|
81
|
-
>({})
|
|
82
|
-
const [instrument, setInstrument] = useState<Soundfont.Player | null>(null)
|
|
83
|
-
|
|
84
|
-
const loadInstrument = useCallback(
|
|
85
|
-
(nm: Soundfont.InstrumentName) => {
|
|
86
|
-
// Re-trigger loading state
|
|
87
|
-
setInstrument(null)
|
|
88
|
-
|
|
89
|
-
if (isDefined(hostname)) {
|
|
90
|
-
// pull from an url
|
|
91
|
-
Soundfont.instrument(audioContext, nm, {
|
|
92
|
-
format,
|
|
93
|
-
soundfont,
|
|
94
|
-
nameToUrl: (name: string, sf: string, fmt: string) =>
|
|
95
|
-
`${hostname}/${sf}/${name}-${fmt}.js`,
|
|
96
|
-
}).then(newInstrument => {
|
|
97
|
-
setInstrument(newInstrument)
|
|
98
|
-
})
|
|
99
|
-
} else if (instrumentName) {
|
|
100
|
-
// the host app has already injected the soundfont into global space
|
|
101
|
-
// Try to create a player directly from our bundled soundfont data
|
|
102
|
-
const soundfontData = window.MIDI?.Soundfont?.[instrumentName]
|
|
103
|
-
if (soundfontData) {
|
|
104
|
-
// MIDI note number to note name conversion using Tonal
|
|
105
|
-
const midiToNoteName = (midiNumber: number | string): string => {
|
|
106
|
-
const num =
|
|
107
|
-
typeof midiNumber === 'string'
|
|
108
|
-
? Number.parseInt(midiNumber, 10)
|
|
109
|
-
: midiNumber
|
|
110
|
-
return Note.fromMidi(num)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Create a simple player that can play notes from our bundled data
|
|
114
|
-
const player = {
|
|
115
|
-
play: (note: string | number, velocity?: number) => {
|
|
116
|
-
// Convert MIDI note number to note name if needed
|
|
117
|
-
const noteName = midiToNoteName(note)
|
|
118
|
-
const noteData = soundfontData[noteName]
|
|
119
|
-
if (noteData) {
|
|
120
|
-
// Create audio from the data URL
|
|
121
|
-
const audio = new Audio(noteData)
|
|
122
|
-
audio.volume = (velocity || 127) / 127
|
|
123
|
-
audio.play()
|
|
124
|
-
|
|
125
|
-
// Return a compatible object with stop method
|
|
126
|
-
return {
|
|
127
|
-
stop: () => {
|
|
128
|
-
audio.pause()
|
|
129
|
-
audio.currentTime = 0
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return null
|
|
134
|
-
},
|
|
135
|
-
stop: () => {
|
|
136
|
-
// Simple stop implementation
|
|
137
|
-
},
|
|
138
|
-
}
|
|
139
|
-
setInstrument(player as Soundfont.Player)
|
|
140
|
-
} else {
|
|
141
|
-
setIsLoading(false)
|
|
142
|
-
}
|
|
143
|
-
} else {
|
|
144
|
-
// sound must be disabled
|
|
145
|
-
setIsLoading(false)
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
[audioContext, format, hostname, instrumentName, soundfont],
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
useEffect(() => {
|
|
152
|
-
if (instrumentName) {
|
|
153
|
-
loadInstrument(instrumentName)
|
|
154
|
-
}
|
|
155
|
-
}, [instrumentName, loadInstrument])
|
|
156
|
-
|
|
157
|
-
useEffect(() => {
|
|
158
|
-
if (isDefined(instrumentName)) {
|
|
159
|
-
setIsLoading(!instrument)
|
|
160
|
-
}
|
|
161
|
-
}, [instrument, instrumentName])
|
|
162
|
-
|
|
163
|
-
const playNote = (midiNumber: number) => {
|
|
164
|
-
if (instrument) {
|
|
165
|
-
audioContext.resume().then(() => {
|
|
166
|
-
const audioNode = instrument.play(String(midiNumber))
|
|
167
|
-
setActiveAudioNodes(prev => ({ ...prev, [midiNumber]: audioNode }))
|
|
168
|
-
})
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const stopNote = (midiNumber: number) => {
|
|
173
|
-
audioContext.resume().then(() => {
|
|
174
|
-
if (!activeAudioNodes[midiNumber]) {
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
const audioNode = activeAudioNodes[midiNumber]
|
|
178
|
-
audioNode.stop()
|
|
179
|
-
setActiveAudioNodes(prev => {
|
|
180
|
-
const { [midiNumber]: _, ...rest } = prev
|
|
181
|
-
return rest
|
|
182
|
-
})
|
|
183
|
-
})
|
|
184
|
-
onChange?.(midiNumber)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Clear any residual notes that don't get called with stopNote
|
|
188
|
-
const stopAllNotes = () => {
|
|
189
|
-
audioContext.resume().then(() => {
|
|
190
|
-
const nodes = Object.values(activeAudioNodes)
|
|
191
|
-
for (const node of nodes) {
|
|
192
|
-
if (node) {
|
|
193
|
-
node.stop()
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
setActiveAudioNodes(nodes)
|
|
197
|
-
})
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return render({
|
|
201
|
-
isLoading,
|
|
202
|
-
playNote,
|
|
203
|
-
stopNote,
|
|
204
|
-
stopAllNotes,
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// https://gleitz.github.io/midi-js-soundfonts/MusyngKite/acoustic_grand_piano-mp3.js
|
|
209
|
-
// https://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/.js
|
|
210
|
-
|
|
211
|
-
export { SoundfontProvider }
|
|
212
|
-
|
|
213
|
-
export type { SoundfontProviderProps }
|
package/lib/other/SvgText.tsx
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
const MEASUREMENT_ELEMENT_ID = '__react_svg_text_measurement_id'
|
|
4
|
-
|
|
5
|
-
type WordsByLine = {
|
|
6
|
-
words: string[]
|
|
7
|
-
width: number
|
|
8
|
-
showLine: boolean
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const calculateWordsByLines = (
|
|
12
|
-
text: string,
|
|
13
|
-
wordWidths: WordWidths,
|
|
14
|
-
maxWidth: number,
|
|
15
|
-
maxHeight: number,
|
|
16
|
-
) => {
|
|
17
|
-
const { lineHeight, spaceWidth, wordsWithComputedWidth } = wordWidths
|
|
18
|
-
|
|
19
|
-
return text.split(/\s+/).reduce((result: WordsByLine[], word) => {
|
|
20
|
-
const wordWidth = wordsWithComputedWidth[word]
|
|
21
|
-
const currentLine = result[result.length - 1]
|
|
22
|
-
if (currentLine && currentLine.width + wordWidth + spaceWidth < maxWidth) {
|
|
23
|
-
// Word can be added to an existing line
|
|
24
|
-
currentLine.words.push(word)
|
|
25
|
-
currentLine.width += wordWidth + spaceWidth
|
|
26
|
-
} else {
|
|
27
|
-
// Add first word to line or word is too long to scaleToFit on existing line
|
|
28
|
-
const newLine: WordsByLine = {
|
|
29
|
-
words: [word],
|
|
30
|
-
width: wordWidth,
|
|
31
|
-
showLine: maxHeight
|
|
32
|
-
? lineHeight * (result.length + 1) < maxHeight
|
|
33
|
-
: true,
|
|
34
|
-
}
|
|
35
|
-
result.push(newLine)
|
|
36
|
-
}
|
|
37
|
-
return result
|
|
38
|
-
}, [])
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
type WordWidths = {
|
|
42
|
-
wordsWithComputedWidth: Record<string, number>
|
|
43
|
-
spaceWidth: number
|
|
44
|
-
lineHeight: number
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const calculateWordWidths = (
|
|
48
|
-
style?: CSSStyleDeclaration,
|
|
49
|
-
textNode?: SVGGraphicsElement | null,
|
|
50
|
-
text?: string,
|
|
51
|
-
): WordWidths | undefined => {
|
|
52
|
-
if (style && textNode) {
|
|
53
|
-
// biome-ignore lint/complexity/noForEach: <explanation>
|
|
54
|
-
Array.from(style).forEach(key =>
|
|
55
|
-
textNode.style.setProperty(
|
|
56
|
-
key,
|
|
57
|
-
style.getPropertyValue(key),
|
|
58
|
-
style.getPropertyPriority(key),
|
|
59
|
-
),
|
|
60
|
-
)
|
|
61
|
-
const wordArray = [...new Set(String(text).split(/\s+/))]
|
|
62
|
-
const wordsWithComputedWidth = wordArray.reduce((wordMap, word) => {
|
|
63
|
-
textNode.textContent = word
|
|
64
|
-
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
|
|
65
|
-
return { ...wordMap, [word]: textNode.getBBox().width }
|
|
66
|
-
}, {})
|
|
67
|
-
|
|
68
|
-
textNode.textContent = '\u00A0'
|
|
69
|
-
|
|
70
|
-
const spaceWidth =
|
|
71
|
-
(textNode as SVGTextContentElement)?.getComputedTextLength?.() || 8
|
|
72
|
-
const lineHeight = textNode.getBBox().height
|
|
73
|
-
|
|
74
|
-
textNode.setAttribute('style', '')
|
|
75
|
-
return { wordsWithComputedWidth, spaceWidth, lineHeight }
|
|
76
|
-
}
|
|
77
|
-
return undefined
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
type TextProps = Partial<HTMLOrSVGElement> & {
|
|
81
|
-
className?: string
|
|
82
|
-
dx?: number
|
|
83
|
-
dy?: number
|
|
84
|
-
maxHeight?: number
|
|
85
|
-
maxWidth?: number
|
|
86
|
-
transform?: string
|
|
87
|
-
text: string
|
|
88
|
-
textAnchor?: React.CSSProperties['textAnchor']
|
|
89
|
-
verticalAnchor?: 'start' | 'middle' | 'end'
|
|
90
|
-
x?: number
|
|
91
|
-
y?: number
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const SvgText = (props: TextProps) => {
|
|
95
|
-
const {
|
|
96
|
-
className = '',
|
|
97
|
-
dx = 0,
|
|
98
|
-
dy = 0,
|
|
99
|
-
maxHeight = 1000,
|
|
100
|
-
maxWidth = 1000,
|
|
101
|
-
// textAnchor = 'start',
|
|
102
|
-
text = '',
|
|
103
|
-
verticalAnchor = 'start',
|
|
104
|
-
x = 0,
|
|
105
|
-
y = 0,
|
|
106
|
-
transform = '',
|
|
107
|
-
...rest
|
|
108
|
-
} = props
|
|
109
|
-
const [wordWidths, setWordWidths] = useState<WordWidths>()
|
|
110
|
-
const [textLines, setTextLines] = useState<WordsByLine[]>([])
|
|
111
|
-
const [style, setComputedStyle] = useState<CSSStyleDeclaration>()
|
|
112
|
-
const measureRef = useRef<SVGSVGElement | HTMLElement>({} as HTMLElement)
|
|
113
|
-
|
|
114
|
-
const displayedLines: WordsByLine[] = useMemo(() => {
|
|
115
|
-
const result = textLines.filter(({ showLine }) => showLine)
|
|
116
|
-
return result.length
|
|
117
|
-
? result
|
|
118
|
-
: text
|
|
119
|
-
.split(/\n/)
|
|
120
|
-
.map(line => ({ words: [line], width: 0, showLine: true }))
|
|
121
|
-
}, [textLines, text])
|
|
122
|
-
|
|
123
|
-
const ref: React.LegacyRef<SVGTextElement> = useCallback(
|
|
124
|
-
(node: SVGTextElement | null) => {
|
|
125
|
-
setComputedStyle(
|
|
126
|
-
node !== null ? window.getComputedStyle(node) : undefined,
|
|
127
|
-
)
|
|
128
|
-
},
|
|
129
|
-
[],
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
const el = document.getElementById(MEASUREMENT_ELEMENT_ID)
|
|
134
|
-
if (el === null) {
|
|
135
|
-
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
136
|
-
svg.setAttribute('id', MEASUREMENT_ELEMENT_ID)
|
|
137
|
-
document.body.appendChild(svg)
|
|
138
|
-
svg.appendChild(
|
|
139
|
-
document.createElementNS('http://www.w3.org/2000/svg', 'text'),
|
|
140
|
-
)
|
|
141
|
-
measureRef.current = svg
|
|
142
|
-
return () => {
|
|
143
|
-
document.body.removeChild(svg)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
measureRef.current = el
|
|
147
|
-
}, [])
|
|
148
|
-
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
const wordWithWidths = calculateWordWidths(
|
|
151
|
-
style,
|
|
152
|
-
measureRef.current as SVGGraphicsElement | null,
|
|
153
|
-
text,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
setWordWidths(wordWithWidths)
|
|
157
|
-
}, [text, style])
|
|
158
|
-
|
|
159
|
-
useEffect(() => {
|
|
160
|
-
if (maxWidth && wordWidths) {
|
|
161
|
-
const lines = text
|
|
162
|
-
.split(/\n/)
|
|
163
|
-
.flatMap(line =>
|
|
164
|
-
calculateWordsByLines(line, wordWidths, maxWidth, maxHeight),
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
setTextLines(lines)
|
|
168
|
-
}
|
|
169
|
-
}, [maxHeight, maxWidth, wordWidths, text])
|
|
170
|
-
|
|
171
|
-
const startDy = useMemo(() => {
|
|
172
|
-
if (wordWidths?.lineHeight) {
|
|
173
|
-
switch (verticalAnchor) {
|
|
174
|
-
case 'start':
|
|
175
|
-
return wordWidths.lineHeight
|
|
176
|
-
|
|
177
|
-
case 'middle':
|
|
178
|
-
return -(((displayedLines.length - 1) * wordWidths.lineHeight) / 2)
|
|
179
|
-
|
|
180
|
-
default:
|
|
181
|
-
return -(displayedLines.length - 1) * wordWidths.lineHeight
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return 0
|
|
185
|
-
}, [displayedLines.length, verticalAnchor, wordWidths?.lineHeight])
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<text
|
|
189
|
-
className={className}
|
|
190
|
-
ref={ref}
|
|
191
|
-
dx={dx}
|
|
192
|
-
dy={dy}
|
|
193
|
-
x={x}
|
|
194
|
-
y={y}
|
|
195
|
-
// textAnchor={textAnchor}
|
|
196
|
-
transform={transform}
|
|
197
|
-
{...rest}>
|
|
198
|
-
<>
|
|
199
|
-
{displayedLines.map((line, idx) => (
|
|
200
|
-
<tspan
|
|
201
|
-
dx={x + dx}
|
|
202
|
-
dy={idx === 0 ? startDy + dy : wordWidths?.lineHeight || 0}
|
|
203
|
-
key={idx}
|
|
204
|
-
x={0}>
|
|
205
|
-
{line.words.join(' ')}
|
|
206
|
-
</tspan>
|
|
207
|
-
))}
|
|
208
|
-
{textLines.length && displayedLines.length !== textLines.length && (
|
|
209
|
-
<>
|
|
210
|
-
<tspan>...</tspan>
|
|
211
|
-
<title>{text}</title>
|
|
212
|
-
</>
|
|
213
|
-
)}
|
|
214
|
-
</>
|
|
215
|
-
</text>
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export { SvgText, MEASUREMENT_ELEMENT_ID }
|
|
220
|
-
|
|
221
|
-
export type { TextProps, WordWidths, WordsByLine }
|
package/lib/pickers/CCPicker.tsx
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { forwardRef } from 'react'
|
|
2
|
-
import type { GroupBase, SelectInstance } from 'react-select'
|
|
3
|
-
import { changeControllerOptions } from '../midi/ccValues'
|
|
4
|
-
import { Select } from './Select'
|
|
5
|
-
import type { Option, SelectProps } from './Select'
|
|
6
|
-
|
|
7
|
-
type CCPickerProps = Omit<SelectProps<number>, 'options'>
|
|
8
|
-
|
|
9
|
-
type CCPickerRef = SelectInstance<
|
|
10
|
-
Option<number>,
|
|
11
|
-
false,
|
|
12
|
-
GroupBase<Option<number>>
|
|
13
|
-
>
|
|
14
|
-
|
|
15
|
-
const CCPicker = forwardRef<
|
|
16
|
-
SelectInstance<Option<number>, false, GroupBase<Option<number>>>,
|
|
17
|
-
CCPickerProps
|
|
18
|
-
>((props, ref) => (
|
|
19
|
-
<Select
|
|
20
|
-
{...props}
|
|
21
|
-
options={changeControllerOptions}
|
|
22
|
-
ref={ref}
|
|
23
|
-
/>
|
|
24
|
-
))
|
|
25
|
-
|
|
26
|
-
export { CCPicker }
|
|
27
|
-
|
|
28
|
-
export type { CCPickerProps, CCPickerRef }
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import cl from 'classnames'
|
|
2
|
-
import { forwardRef, useCallback, useEffect, useState } from 'react'
|
|
3
|
-
import type { GroupBase, SelectInstance } from 'react-select'
|
|
4
|
-
import { arraySequence } from '../utils'
|
|
5
|
-
import { Select, noSelection } from './Select'
|
|
6
|
-
import type { Option, SelectProps } from './Select'
|
|
7
|
-
|
|
8
|
-
type InternalValue = number | 'separator'
|
|
9
|
-
// type ExternalValue = number
|
|
10
|
-
|
|
11
|
-
type ChannelMappingOption = Option<InternalValue>
|
|
12
|
-
|
|
13
|
-
const defaultOptions: ChannelMappingOption[] = arraySequence(16).map(value => ({
|
|
14
|
-
value,
|
|
15
|
-
label: value,
|
|
16
|
-
}))
|
|
17
|
-
|
|
18
|
-
interface ChannelMappingPickerProps
|
|
19
|
-
extends Omit<SelectProps<InternalValue>, 'options'> {
|
|
20
|
-
channels: Option<number>[]
|
|
21
|
-
className?: string
|
|
22
|
-
filterOptions?: (
|
|
23
|
-
candidate: Option<InternalValue>,
|
|
24
|
-
inputValue: string,
|
|
25
|
-
) => boolean
|
|
26
|
-
value: number
|
|
27
|
-
onChange: (value: InternalValue) => void
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type ChannelMappingPickerRef = SelectInstance<
|
|
31
|
-
Option<number>,
|
|
32
|
-
false,
|
|
33
|
-
GroupBase<Option<number>>
|
|
34
|
-
>
|
|
35
|
-
|
|
36
|
-
type FormatOptionLabelFunction = NonNullable<
|
|
37
|
-
SelectProps<InternalValue>['formatOptionLabel']
|
|
38
|
-
>
|
|
39
|
-
|
|
40
|
-
const ChannelMappingPicker = forwardRef<
|
|
41
|
-
SelectInstance<
|
|
42
|
-
Option<InternalValue>,
|
|
43
|
-
false,
|
|
44
|
-
GroupBase<Option<InternalValue>>
|
|
45
|
-
>,
|
|
46
|
-
ChannelMappingPickerProps
|
|
47
|
-
>(({ channels, className, ...rest }, ref) => {
|
|
48
|
-
const [options, setOptions] = useState<Option<InternalValue>[]>([])
|
|
49
|
-
|
|
50
|
-
const formatOptionLabel: FormatOptionLabelFunction = useCallback(
|
|
51
|
-
({ value, label }, { context }) => {
|
|
52
|
-
if (value === 'separator') {
|
|
53
|
-
return <span className="zds-mappings-user-mapping">{label}</span>
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (typeof value !== 'number') return null
|
|
57
|
-
|
|
58
|
-
const mapping = channels[value]
|
|
59
|
-
|
|
60
|
-
return mapping ? (
|
|
61
|
-
<span className="mapping-entry">
|
|
62
|
-
<b>
|
|
63
|
-
{value !== noSelection && (
|
|
64
|
-
<>
|
|
65
|
-
{value + 1}
|
|
66
|
-
{context === 'value' && ' -'}
|
|
67
|
-
</>
|
|
68
|
-
)}
|
|
69
|
-
</b>
|
|
70
|
-
<span className="mapping-entry-label">{mapping.label}</span>
|
|
71
|
-
</span>
|
|
72
|
-
) : (
|
|
73
|
-
<span className="mapping-entry empty-mapping-entry">
|
|
74
|
-
<b>
|
|
75
|
-
{value !== noSelection && (
|
|
76
|
-
<>
|
|
77
|
-
{value + 1}
|
|
78
|
-
{context === 'value' && ' -'}
|
|
79
|
-
</>
|
|
80
|
-
)}
|
|
81
|
-
</b>
|
|
82
|
-
<span className="mapping-entry-label">no mapping</span>
|
|
83
|
-
</span>
|
|
84
|
-
)
|
|
85
|
-
},
|
|
86
|
-
[channels],
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
const result = defaultOptions.sort(
|
|
91
|
-
(a: ChannelMappingOption, b: ChannelMappingOption) => {
|
|
92
|
-
if (
|
|
93
|
-
typeof a.value === 'number' &&
|
|
94
|
-
typeof b.value === 'number' &&
|
|
95
|
-
channels[a.value] &&
|
|
96
|
-
!channels[b.value]
|
|
97
|
-
) {
|
|
98
|
-
return -1
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
typeof a.value === 'number' &&
|
|
103
|
-
typeof b.value === 'number' &&
|
|
104
|
-
!channels[a.value] &&
|
|
105
|
-
channels[b.value]
|
|
106
|
-
) {
|
|
107
|
-
return 1
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (typeof a.value === 'number' && typeof b.value === 'number') {
|
|
111
|
-
return a.value - b.value
|
|
112
|
-
}
|
|
113
|
-
return 0
|
|
114
|
-
},
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
const numFilled = result.filter(
|
|
118
|
-
({ value }) =>
|
|
119
|
-
(typeof value === 'number' && !!channels[value]) ||
|
|
120
|
-
value === noSelection,
|
|
121
|
-
).length
|
|
122
|
-
|
|
123
|
-
const separatorExists = result.some(({ value }) => value === 'separator')
|
|
124
|
-
|
|
125
|
-
if (numFilled && numFilled < 16 && !separatorExists) {
|
|
126
|
-
// insert a separator after the count of filled items
|
|
127
|
-
result.splice(numFilled, 0, { label: <hr />, value: 'separator' })
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
setOptions(result)
|
|
131
|
-
}, [channels])
|
|
132
|
-
|
|
133
|
-
return (
|
|
134
|
-
<Select<InternalValue>
|
|
135
|
-
{...rest}
|
|
136
|
-
className={cl(className, 'channel-mapping-picker')}
|
|
137
|
-
formatOptionLabel={formatOptionLabel}
|
|
138
|
-
options={options}
|
|
139
|
-
ref={ref}
|
|
140
|
-
/>
|
|
141
|
-
)
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
export { ChannelMappingPicker }
|
|
145
|
-
|
|
146
|
-
export type {
|
|
147
|
-
ChannelMappingOption,
|
|
148
|
-
ChannelMappingPickerProps,
|
|
149
|
-
ChannelMappingPickerRef,
|
|
150
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import cl from 'classnames'
|
|
2
|
-
import { forwardRef, useCallback } from 'react'
|
|
3
|
-
import type { GroupBase, SelectInstance } from 'react-select'
|
|
4
|
-
import { arraySequence } from '../utils'
|
|
5
|
-
import { Select } from './Select'
|
|
6
|
-
import { type Option, type SelectProps, noSelection } from './Select'
|
|
7
|
-
|
|
8
|
-
type Channel =
|
|
9
|
-
| 0
|
|
10
|
-
| 1
|
|
11
|
-
| 2
|
|
12
|
-
| 3
|
|
13
|
-
| 4
|
|
14
|
-
| 5
|
|
15
|
-
| 6
|
|
16
|
-
| 7
|
|
17
|
-
| 8
|
|
18
|
-
| 9
|
|
19
|
-
| 10
|
|
20
|
-
| 11
|
|
21
|
-
| 12
|
|
22
|
-
| 13
|
|
23
|
-
| 14
|
|
24
|
-
| 15
|
|
25
|
-
| typeof noSelection
|
|
26
|
-
|
|
27
|
-
interface FormatOptionLabelContext {
|
|
28
|
-
context: 'menu' | 'value'
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type ChannelPickerProps = Omit<SelectProps<Channel>, 'options'>
|
|
32
|
-
|
|
33
|
-
type ChannelPickerRef = SelectInstance<
|
|
34
|
-
Option<Channel>,
|
|
35
|
-
false,
|
|
36
|
-
GroupBase<Option<Channel>>
|
|
37
|
-
>
|
|
38
|
-
|
|
39
|
-
const options: Option<Channel>[] = arraySequence(16).map(value => ({
|
|
40
|
-
value: value as Channel,
|
|
41
|
-
label: `Channel ${value + 1}`,
|
|
42
|
-
}))
|
|
43
|
-
|
|
44
|
-
const filterOptions = (option: Option<Channel>, input: string) => {
|
|
45
|
-
// console.log(option, input)
|
|
46
|
-
|
|
47
|
-
if (input) {
|
|
48
|
-
return String(option.label).toLowerCase().includes(input.toLowerCase())
|
|
49
|
-
}
|
|
50
|
-
return true
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const ChannelPicker = forwardRef<
|
|
54
|
-
SelectInstance<Option<Channel>, false, GroupBase<Option<Channel>>>,
|
|
55
|
-
ChannelPickerProps
|
|
56
|
-
>(({ className, value = noSelection, ...rest }, ref) => {
|
|
57
|
-
const formatOptionLabel = useCallback(
|
|
58
|
-
(option: Option<unknown>, { context }: FormatOptionLabelContext) => {
|
|
59
|
-
if (value === undefined) {
|
|
60
|
-
return '--'
|
|
61
|
-
}
|
|
62
|
-
return context === 'value'
|
|
63
|
-
? option.label
|
|
64
|
-
: Number.parseInt(String(option.value)) + 1 // menu, e.g. the list of options
|
|
65
|
-
},
|
|
66
|
-
[value],
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<Select
|
|
71
|
-
{...rest}
|
|
72
|
-
className={cl(className, 'channel-picker')}
|
|
73
|
-
filterOptions={filterOptions}
|
|
74
|
-
formatOptionLabel={formatOptionLabel}
|
|
75
|
-
options={options}
|
|
76
|
-
value={value}
|
|
77
|
-
ref={ref}
|
|
78
|
-
/>
|
|
79
|
-
)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
export { ChannelPicker }
|
|
83
|
-
|
|
84
|
-
export type { Channel, ChannelPickerProps, ChannelPickerRef }
|