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.
Files changed (68) hide show
  1. package/dist/index.cjs.js +9 -9
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.es.js +56 -57
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/other/SoundFontProvider.js +5 -4
  6. package/dist/pickers/KeyPicker.js +1 -2
  7. package/dist/types/other/SoundFontProvider.d.ts +1 -1
  8. package/dist/types/pickers/KeyPicker.d.ts +1 -4
  9. package/lib/soundfonts/acoustic_grand_piano-mp3.js +107 -0
  10. package/package.json +5 -1
  11. package/.editorconfig +0 -19
  12. package/.github/workflows/codeql-analysis.yml +0 -67
  13. package/.nvmrc +0 -1
  14. package/.prettierignore +0 -0
  15. package/.prettierrc +0 -13
  16. package/.storybook/main.ts +0 -42
  17. package/.storybook/manager.ts +0 -12
  18. package/.storybook/preview.tsx +0 -26
  19. package/.vscode/extensions.json +0 -32
  20. package/.vscode/javascript.code-snippets +0 -229
  21. package/.vscode/settings.json +0 -358
  22. package/biome.jsonc +0 -158
  23. package/lib/@types/nebiru-react-rotary-knob.d.ts +0 -20
  24. package/lib/@types/react-svgmt.d.ts +0 -74
  25. package/lib/@types/zds-mappings.d.ts +0 -55
  26. package/lib/@types/zds-react-piano.d.ts +0 -17
  27. package/lib/hooks/useComponentSize.ts +0 -52
  28. package/lib/hooks/useStateWithDynamicDefault.ts +0 -13
  29. package/lib/index.ts +0 -52
  30. package/lib/midi/ccValues.ts +0 -139
  31. package/lib/midi/export.ts +0 -88
  32. package/lib/other/DefaultTooltip.ts +0 -14
  33. package/lib/other/OctavePlayer.tsx +0 -139
  34. package/lib/other/SoundFontProvider.tsx +0 -213
  35. package/lib/other/SvgText.tsx +0 -221
  36. package/lib/pickers/CCPicker.tsx +0 -28
  37. package/lib/pickers/ChannelMappingPicker.tsx +0 -150
  38. package/lib/pickers/ChannelPicker.tsx +0 -84
  39. package/lib/pickers/KeyPicker.tsx +0 -56
  40. package/lib/pickers/Knob.tsx +0 -93
  41. package/lib/pickers/KnobPicker.tsx +0 -93
  42. package/lib/pickers/LatchPicker.tsx +0 -33
  43. package/lib/pickers/MappingPicker.tsx +0 -68
  44. package/lib/pickers/NotePicker.tsx +0 -95
  45. package/lib/pickers/PianoPicker.tsx +0 -81
  46. package/lib/pickers/PolarityPicker.tsx +0 -39
  47. package/lib/pickers/ResponseCurve.tsx +0 -251
  48. package/lib/pickers/ResponseCurvePicker.tsx +0 -75
  49. package/lib/pickers/Select.tsx +0 -268
  50. package/lib/pickers/StatusPicker.tsx +0 -30
  51. package/lib/pickers/ValuePicker.tsx +0 -67
  52. package/lib/pickers/knobSkin10.tsx +0 -112
  53. package/lib/rotaryKnob/InternalInput.tsx +0 -43
  54. package/lib/rotaryKnob/Types.ts +0 -6
  55. package/lib/rotaryKnob/helpers/DrawCircle.tsx +0 -41
  56. package/lib/rotaryKnob/helpers/DrawLine.tsx +0 -32
  57. package/lib/rotaryKnob/helpers/HelpersOverlay.tsx +0 -43
  58. package/lib/rotaryKnob/helpers/KnobVisualHelpers.tsx +0 -138
  59. package/lib/rotaryKnob/index.tsx +0 -417
  60. package/lib/rotaryKnob/knobdefaultskin.ts +0 -69
  61. package/lib/rotaryKnob/utils.ts +0 -112
  62. package/lib/utils.ts +0 -41
  63. package/stories/assets/sampleStyles.css +0 -365
  64. package/stories/assets/utils.js +0 -7
  65. package/stories/picker.stories.tsx +0 -432
  66. package/tsconfig.json +0 -35
  67. package/vite.config.ts +0 -41
  68. /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 }
@@ -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 }
@@ -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 }