zds-pickers 4.0.5 → 4.0.6

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 (74) hide show
  1. package/.editorconfig +19 -0
  2. package/.github/workflows/codeql-analysis.yml +67 -0
  3. package/.nvmrc +1 -0
  4. package/.prettierignore +0 -0
  5. package/.prettierrc +13 -0
  6. package/.storybook/main.ts +42 -0
  7. package/.storybook/manager.ts +12 -0
  8. package/.storybook/preview.tsx +26 -0
  9. package/.vscode/extensions.json +32 -0
  10. package/.vscode/javascript.code-snippets +229 -0
  11. package/.vscode/settings.json +358 -0
  12. package/biome.jsonc +158 -0
  13. package/dist/index.es.js +3 -1
  14. package/dist/types/index.d.ts +266 -2
  15. package/dist/types/pickers/CCPicker.d.ts +2 -1
  16. package/dist/types/pickers/ChannelMappingPicker.d.ts +2 -1
  17. package/dist/types/pickers/ChannelPicker.d.ts +5 -3
  18. package/dist/types/pickers/LatchPicker.d.ts +5 -3
  19. package/dist/types/pickers/MappingPicker.d.ts +2 -1
  20. package/dist/types/pickers/NotePicker.d.ts +2 -1
  21. package/dist/types/pickers/PolarityPicker.d.ts +6 -4
  22. package/dist/types/pickers/ResponseCurve.d.ts +14 -5
  23. package/dist/types/pickers/ResponseCurvePicker.d.ts +6 -4
  24. package/dist/types/pickers/StatusPicker.d.ts +2 -1
  25. package/dist/types/pickers/ValuePicker.d.ts +2 -1
  26. package/lib/@types/nebiru-react-rotary-knob.d.ts +20 -0
  27. package/lib/@types/react-svgmt.d.ts +74 -0
  28. package/lib/@types/zds-mappings.d.ts +55 -0
  29. package/lib/@types/zds-react-piano.d.ts +17 -0
  30. package/lib/hooks/useComponentSize.ts +52 -0
  31. package/lib/hooks/useStateWithDynamicDefault.ts +13 -0
  32. package/lib/main.ts +34 -0
  33. package/lib/midi/ccValues.ts +139 -0
  34. package/lib/midi/export.ts +54 -0
  35. package/lib/other/DefaultTooltip.ts +14 -0
  36. package/lib/other/OctavePlayer.tsx +141 -0
  37. package/lib/other/SVGText.tsx +221 -0
  38. package/lib/other/SoundFontProvider.tsx +130 -0
  39. package/lib/pickers/CCPicker.tsx +28 -0
  40. package/lib/pickers/ChannelMappingPicker.tsx +135 -0
  41. package/lib/pickers/ChannelPicker.tsx +76 -0
  42. package/lib/pickers/Knob.tsx +93 -0
  43. package/lib/pickers/KnobPicker.tsx +93 -0
  44. package/lib/pickers/LatchPicker.tsx +33 -0
  45. package/lib/pickers/MappingPicker.tsx +68 -0
  46. package/lib/pickers/NotePicker.tsx +95 -0
  47. package/lib/pickers/PianoPicker.tsx +77 -0
  48. package/lib/pickers/PolarityPicker.tsx +39 -0
  49. package/lib/pickers/ResponseCurve.tsx +304 -0
  50. package/lib/pickers/ResponseCurvePicker.tsx +74 -0
  51. package/lib/pickers/Select.tsx +248 -0
  52. package/lib/pickers/StatusPicker.tsx +28 -0
  53. package/lib/pickers/ValuePicker.tsx +67 -0
  54. package/lib/pickers/knobSkin10.tsx +94 -0
  55. package/lib/rotaryKnob/InternalInput.tsx +43 -0
  56. package/lib/rotaryKnob/Types.ts +6 -0
  57. package/lib/rotaryKnob/helpers/DrawCircle.tsx +41 -0
  58. package/lib/rotaryKnob/helpers/DrawLine.tsx +32 -0
  59. package/lib/rotaryKnob/helpers/HelpersOverlay.tsx +43 -0
  60. package/lib/rotaryKnob/helpers/KnobVisualHelpers.tsx +138 -0
  61. package/lib/rotaryKnob/index.tsx +417 -0
  62. package/lib/rotaryKnob/knobdefaultskin.ts +56 -0
  63. package/lib/rotaryKnob/utils.ts +112 -0
  64. package/lib/temp/acoustic_grand_piano-mp3.js +94 -0
  65. package/lib/utils.ts +41 -0
  66. package/package.json +2 -6
  67. package/stories/assets/sampleStyles.css +349 -0
  68. package/stories/assets/utils.js +7 -0
  69. package/stories/picker.stories.tsx +313 -0
  70. package/tsconfig.app.json +26 -0
  71. package/tsconfig.json +34 -0
  72. package/tsconfig.node.json +24 -0
  73. package/vite.config.ts +45 -0
  74. package/dist/index.d.ts +0 -244
@@ -0,0 +1,130 @@
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 '../temp/acoustic_grand_piano-mp3'
6
+
7
+ const isDefined = <T,>(item: T) =>
8
+ item !== undefined && item !== null && !Number.isNaN(item)
9
+
10
+ type SoundfontProviderProps = {
11
+ audioContext: AudioContext
12
+ format?: 'mp3' | 'ogg'
13
+ hostname?: string
14
+ instrumentName: Soundfont.InstrumentName
15
+ onChange?: (midiNumber: number) => void
16
+ render: (props: {
17
+ isLoading: boolean
18
+ playNote: (midiNumber: number) => void
19
+ stopNote: (midiNumber: number) => void
20
+ stopAllNotes: () => void
21
+ }) => React.ReactNode
22
+ soundfont?: 'MusyngKite' | 'FluidR3_GM'
23
+ }
24
+
25
+ const SoundfontProvider = (props: SoundfontProviderProps) => {
26
+ const {
27
+ audioContext,
28
+ format,
29
+ hostname,
30
+ instrumentName,
31
+ onChange,
32
+ render,
33
+ soundfont,
34
+ } = props
35
+
36
+ const [isLoading, setIsLoading] = useState(true)
37
+ const [activeAudioNodes, setActiveAudioNodes] = useState<
38
+ Record<number, Soundfont.Player>
39
+ >({})
40
+ const [instrument, setInstrument] = useState<Soundfont.Player | null>(null)
41
+
42
+ const loadInstrument = useCallback(
43
+ (nm: Soundfont.InstrumentName) => {
44
+ // Re-trigger loading state
45
+ setInstrument(null)
46
+
47
+ if (isDefined(hostname)) {
48
+ // pull from an url
49
+ Soundfont.instrument(audioContext, nm, {
50
+ format,
51
+ soundfont,
52
+ nameToUrl: (name: string, sf: string, fmt: string) =>
53
+ `${hostname}/${sf}/${name}-${fmt}.js`,
54
+ }).then(newInstrument => {
55
+ setInstrument(newInstrument)
56
+ })
57
+ } else if (isDefined(instrumentName)) {
58
+ // the host app has already injected the soundfont into global space
59
+ Soundfont.instrument(audioContext, instrumentName).then(
60
+ newInstrument => {
61
+ setInstrument(newInstrument)
62
+ },
63
+ )
64
+ } else {
65
+ // sound must be disabled
66
+ setIsLoading(false)
67
+ }
68
+ },
69
+ [audioContext, format, hostname, instrumentName, soundfont],
70
+ )
71
+
72
+ useEffect(() => {
73
+ loadInstrument(instrumentName)
74
+ }, [instrumentName, loadInstrument])
75
+
76
+ useEffect(() => {
77
+ if (isDefined(instrumentName)) {
78
+ setIsLoading(!instrument)
79
+ }
80
+ }, [instrument, instrumentName])
81
+
82
+ const playNote = (midiNumber: number) => {
83
+ if (instrument) {
84
+ audioContext.resume().then(() => {
85
+ const audioNode = instrument.play(String(midiNumber))
86
+ setActiveAudioNodes(prev => ({ ...prev, [midiNumber]: audioNode }))
87
+ })
88
+ }
89
+ }
90
+
91
+ const stopNote = (midiNumber: number) => {
92
+ audioContext.resume().then(() => {
93
+ if (!activeAudioNodes[midiNumber]) {
94
+ return
95
+ }
96
+ const audioNode = activeAudioNodes[midiNumber]
97
+ audioNode.stop()
98
+ setActiveAudioNodes(prev => {
99
+ const { [midiNumber]: _, ...rest } = prev
100
+ return rest
101
+ })
102
+ })
103
+ onChange?.(midiNumber)
104
+ }
105
+
106
+ // Clear any residual notes that don't get called with stopNote
107
+ const stopAllNotes = () => {
108
+ audioContext.resume().then(() => {
109
+ const nodes = Object.values(activeAudioNodes)
110
+ for (const node of nodes) {
111
+ if (node) {
112
+ node.stop()
113
+ }
114
+ }
115
+ setActiveAudioNodes(nodes)
116
+ })
117
+ }
118
+
119
+ return render({
120
+ isLoading,
121
+ playNote,
122
+ stopNote,
123
+ stopAllNotes,
124
+ })
125
+ }
126
+
127
+ // https://gleitz.github.io/midi-js-soundfonts/MusyngKite/acoustic_grand_piano-mp3.js
128
+ // https://gleitz.github.io/midi-js-soundfonts/FluidR3_GM/.js
129
+
130
+ export default SoundfontProvider
@@ -0,0 +1,28 @@
1
+ import { forwardRef } from 'react'
2
+ import type { GroupBase, SelectInstance } from 'react-select'
3
+ import ccValues from '../midi/ccValues'
4
+ import Select from './Select'
5
+ import type { Option, SelectProps } from './Select'
6
+
7
+ type CCPickerProps = SelectProps<number>
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={ccValues}
22
+ ref={ref}
23
+ />
24
+ ))
25
+
26
+ export default CCPicker
27
+
28
+ export type { CCPickerProps, CCPickerRef }
@@ -0,0 +1,135 @@
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 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 extends SelectProps<InternalValue> {
19
+ channels: Option<number>[]
20
+ className?: string
21
+ filterOptions?: (
22
+ candidate: Option<InternalValue>,
23
+ inputValue: string,
24
+ ) => boolean
25
+ value: number
26
+ onChange: (value: InternalValue) => void
27
+ }
28
+
29
+ type ChannelMappingPickerRef = SelectInstance<
30
+ Option<number>,
31
+ false,
32
+ GroupBase<Option<number>>
33
+ >
34
+
35
+ type FormatOptionLabelFunction = NonNullable<
36
+ SelectProps<InternalValue>['formatOptionLabel']
37
+ >
38
+
39
+ const ChannelMappingPicker = forwardRef<
40
+ SelectInstance<
41
+ Option<InternalValue>,
42
+ false,
43
+ GroupBase<Option<InternalValue>>
44
+ >,
45
+ ChannelMappingPickerProps
46
+ >(({ channels, className, ...rest }, ref) => {
47
+ const [options, setOptions] = useState<Option<InternalValue>[]>([])
48
+
49
+ const formatOptionLabel: FormatOptionLabelFunction = useCallback(
50
+ ({ value, label }, { context }) => {
51
+ if (value === 'separator') {
52
+ return <span className="zds-mappings-user-mapping">{label}</span>
53
+ }
54
+
55
+ if (typeof value !== 'number') return null
56
+
57
+ const mapping = channels[value]
58
+
59
+ return mapping ? (
60
+ <span className="mapping-entry">
61
+ <b>
62
+ {value + 1}
63
+ {context === 'value' && ' -'}
64
+ </b>
65
+ <span className="mapping-entry-label">{mapping.label}</span>
66
+ </span>
67
+ ) : (
68
+ <span className="mapping-entry empty-mapping-entry">
69
+ <b>
70
+ {value + 1}
71
+ {context === 'value' && ' -'}
72
+ </b>
73
+ <span className="mapping-entry-label">no mapping</span>
74
+ </span>
75
+ )
76
+ },
77
+ [channels],
78
+ )
79
+
80
+ useEffect(() => {
81
+ const result = defaultOptions.sort(
82
+ (a: ChannelMappingOption, b: ChannelMappingOption) => {
83
+ if (
84
+ typeof a.value === 'number' &&
85
+ typeof b.value === 'number' &&
86
+ channels[a.value] &&
87
+ !channels[b.value]
88
+ ) {
89
+ return -1
90
+ }
91
+
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 (typeof a.value === 'number' && typeof b.value === 'number') {
102
+ return a.value - b.value
103
+ }
104
+ return 0
105
+ },
106
+ )
107
+
108
+ const numFilled = result.filter(
109
+ ({ value }) => typeof value === 'number' && !!channels[value],
110
+ ).length
111
+
112
+ const separatorExists = result.some(({ value }) => value === 'separator')
113
+
114
+ if (numFilled && numFilled < 16 && !separatorExists) {
115
+ // insert a separator after the count of filled items
116
+ result.splice(numFilled, 0, { label: <hr />, value: 'separator' })
117
+ }
118
+
119
+ setOptions(result)
120
+ }, [channels])
121
+
122
+ return (
123
+ <Select<InternalValue>
124
+ {...rest}
125
+ className={cl(className, 'channel-mapping-picker')}
126
+ formatOptionLabel={formatOptionLabel}
127
+ options={options}
128
+ ref={ref}
129
+ />
130
+ )
131
+ })
132
+
133
+ export default ChannelMappingPicker
134
+
135
+ export type { ChannelMappingPickerProps, ChannelMappingPickerRef }
@@ -0,0 +1,76 @@
1
+ import cl from 'classnames'
2
+ import { forwardRef } from 'react'
3
+ import type { GroupBase, SelectInstance } from 'react-select'
4
+ import { arraySequence } from '../utils'
5
+ import Select from './Select'
6
+ import type { Option, SelectProps } 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
+
26
+ interface FormatOptionLabelContext {
27
+ context: 'menu' | 'value'
28
+ }
29
+
30
+ type ChannelPickerProps = SelectProps<Channel>
31
+
32
+ type ChannelPickerRef = SelectInstance<
33
+ Option<Channel>,
34
+ false,
35
+ GroupBase<Option<Channel>>
36
+ >
37
+
38
+ const options: Option<Channel>[] = arraySequence(16).map(value => ({
39
+ value: value as Channel,
40
+ label: `Channel ${value + 1}`,
41
+ }))
42
+
43
+ const formatOptionLabel = (
44
+ option: Option<unknown>,
45
+ { context }: FormatOptionLabelContext,
46
+ ) =>
47
+ context === 'value' ? option.label : Number.parseInt(String(option.value)) + 1 // menu, e.g. the list of options
48
+
49
+ const filterOptions = (option: Option<Channel>, input: string) => {
50
+ // console.log(option, input)
51
+
52
+ if (input) {
53
+ return String(option.label).toLowerCase().includes(input.toLowerCase())
54
+ }
55
+ return true
56
+ }
57
+
58
+ const ChannelPicker = forwardRef<
59
+ SelectInstance<Option<Channel>, false, GroupBase<Option<Channel>>>,
60
+ ChannelPickerProps
61
+ >(({ className, ...rest }, ref) => {
62
+ return (
63
+ <Select
64
+ {...rest}
65
+ className={cl(className, 'channel-picker')}
66
+ filterOptions={filterOptions}
67
+ formatOptionLabel={formatOptionLabel}
68
+ options={options}
69
+ ref={ref}
70
+ />
71
+ )
72
+ })
73
+
74
+ export default ChannelPicker
75
+
76
+ export type { Channel, ChannelPickerProps, ChannelPickerRef }
@@ -0,0 +1,93 @@
1
+ import { useCallback } from 'react'
2
+ import useStateWithDynamicDefault from '../hooks/useStateWithDynamicDefault'
3
+ import RotaryKnob from '../rotaryKnob'
4
+ import { assertRange } from '../utils'
5
+ import knobSkin10 from './knobSkin10'
6
+
7
+ type KnobProps = {
8
+ disabled?: boolean
9
+ max?: number
10
+ min?: number
11
+ onChange?: (value: number) => void
12
+ value?: number
13
+ wheelEnabled?: boolean
14
+ wheelSensitivity?: number
15
+ }
16
+
17
+ const Knob = (props: KnobProps) => {
18
+ const {
19
+ disabled = false,
20
+ max = 127,
21
+ min = 0,
22
+ onChange,
23
+ value: initialValue = 0,
24
+ wheelEnabled = false,
25
+ wheelSensitivity = 0.1,
26
+ ...rest
27
+ } = props
28
+
29
+ const [value, setValue] = useStateWithDynamicDefault(initialValue)
30
+
31
+ const handleChange = (val: number) => {
32
+ if (disabled) return
33
+
34
+ const newValue = assertRange(Number.parseInt(String(val), 10), max, min)
35
+ setValue(newValue)
36
+ if (onChange) {
37
+ onChange(newValue)
38
+ }
39
+ }
40
+
41
+ const handleWheel = useCallback(
42
+ (event: React.WheelEvent<HTMLDivElement>) => {
43
+ if (!wheelEnabled || disabled) return
44
+
45
+ event.preventDefault()
46
+ const delta = -event.deltaY * wheelSensitivity
47
+ const newValue = assertRange(value + delta, max, min)
48
+
49
+ setValue(newValue)
50
+ if (onChange) {
51
+ onChange(newValue)
52
+ }
53
+ },
54
+ [
55
+ wheelEnabled,
56
+ disabled,
57
+ wheelSensitivity,
58
+ value,
59
+ max,
60
+ min,
61
+ onChange,
62
+ setValue,
63
+ ],
64
+ )
65
+
66
+ return (
67
+ <RotaryKnob
68
+ clampMax={320}
69
+ clampMin={40}
70
+ className="zds-pickers__knob-container"
71
+ disabled={disabled}
72
+ onChange={handleChange}
73
+ onWheel={wheelEnabled ? handleWheel : undefined}
74
+ preciseMode={false}
75
+ rotateDegrees={180}
76
+ /**
77
+ * To see all skins:
78
+ * http://react-rotary-knob-skins-preview.surge.sh/
79
+ *
80
+ * NOTE: we've copied the one we use into this package because the skins
81
+ * pack package does not work in Electron.
82
+ */
83
+ skin={knobSkin10}
84
+ style={{ opacity: disabled ? 0.4 : 1 }}
85
+ unlockDistance={0}
86
+ {...{ defaultValue: value, max, min, value, ...rest }}
87
+ />
88
+ )
89
+ }
90
+
91
+ export default Knob
92
+
93
+ export type { KnobProps }
@@ -0,0 +1,93 @@
1
+ import { forwardRef, useMemo } from 'react'
2
+ import type { GroupBase, SelectInstance } from 'react-select'
3
+ import { arraySequence } from '../utils'
4
+ import Knob from './Knob'
5
+ import Select from './Select'
6
+ import type { Option, SelectProps } from './Select'
7
+
8
+ type KnobPickerProps = SelectProps<number> & {
9
+ highToLow?: boolean
10
+ includeLabel?: boolean
11
+ includePicker?: boolean
12
+ knobProps?: Partial<KnobPickerProps>
13
+ max?: number
14
+ min?: number
15
+ wheelEnabled?: boolean
16
+ wheelSensitivity?: number
17
+ value: number
18
+ }
19
+
20
+ type KnobPickerRef = SelectInstance<
21
+ Option<number>,
22
+ false,
23
+ GroupBase<Option<number>>
24
+ >
25
+
26
+ const KnobPicker = forwardRef<
27
+ SelectInstance<Option<number>, false, GroupBase<Option<number>>>,
28
+ KnobPickerProps
29
+ >((props, ref) => {
30
+ const {
31
+ disabled,
32
+ highToLow,
33
+ includeLabel,
34
+ includePicker,
35
+ knobProps,
36
+ label,
37
+ max = 127,
38
+ min = 0,
39
+ onChange,
40
+ shrinkLabel,
41
+ value = 0,
42
+ wheelEnabled,
43
+ wheelSensitivity = 0.1,
44
+ ...rest
45
+ } = props
46
+
47
+ const options = useMemo(() => {
48
+ const result = arraySequence(max - min + 1)
49
+ .map(i => min + i)
50
+ .map((i): Option<number> => ({ value: i, label: String(i) }))
51
+ return highToLow ? result.reverse() : result
52
+ }, [highToLow, max, min])
53
+
54
+ return (
55
+ <div className="zds-pickers__container">
56
+ {Boolean(!shrinkLabel && Boolean(label)) && (
57
+ <span className="zds-pickers__label">{label}</span>
58
+ )}
59
+ <div style={{ display: 'inline-flex', gap: 10, alignItems: 'center' }}>
60
+ {Boolean(includePicker) && (
61
+ <Select
62
+ {...{
63
+ ...rest,
64
+ disabled,
65
+ isDisabled: disabled,
66
+ onChange,
67
+ options,
68
+ ref,
69
+ value,
70
+ }}
71
+ />
72
+ )}
73
+
74
+ <Knob
75
+ {...{
76
+ disabled,
77
+ max,
78
+ min,
79
+ onChange,
80
+ value,
81
+ wheelEnabled,
82
+ wheelSensitivity,
83
+ ...knobProps,
84
+ }}
85
+ />
86
+ </div>
87
+ </div>
88
+ )
89
+ })
90
+
91
+ export default KnobPicker
92
+
93
+ export type { KnobPickerProps, KnobPickerRef }
@@ -0,0 +1,33 @@
1
+ import { forwardRef } from 'react'
2
+ import type { GroupBase, SelectInstance } from 'react-select'
3
+ import type { Polarity } from './PolarityPicker'
4
+ import Select from './Select'
5
+ import type { Option, SelectProps } from './Select'
6
+
7
+ const options: Option<Polarity>[] = [
8
+ { value: 0, label: 'Momentary' },
9
+ { value: 1, label: 'Latching' },
10
+ ]
11
+
12
+ type LatchPickerProps = SelectProps<Polarity>
13
+
14
+ type LatchPickerRef = SelectInstance<
15
+ Option<Polarity>,
16
+ false,
17
+ GroupBase<Option<Polarity>>
18
+ >
19
+
20
+ const LatchPicker = forwardRef<
21
+ SelectInstance<Option<Polarity>, false, GroupBase<Option<Polarity>>>,
22
+ LatchPickerProps
23
+ >((props, ref) => (
24
+ <Select
25
+ {...props}
26
+ options={options}
27
+ ref={ref}
28
+ />
29
+ ))
30
+
31
+ export default LatchPicker
32
+
33
+ export type { LatchPickerProps, LatchPickerRef }
@@ -0,0 +1,68 @@
1
+ import { forwardRef, useMemo } from 'react'
2
+ import type { GroupBase, SelectInstance } from 'react-select'
3
+ import { getStockNames, getUserMappingNames } from 'zds-mappings'
4
+ import Select from './Select'
5
+ import type { Option, SelectProps } from './Select'
6
+
7
+ type MappingPickerProps = SelectProps<string> & {
8
+ allowClearing: boolean
9
+ }
10
+
11
+ type MappingPickerRef = SelectInstance<
12
+ Option<string>,
13
+ false,
14
+ GroupBase<Option<string>>
15
+ >
16
+
17
+ const MappingPicker = forwardRef<
18
+ SelectInstance<Option<string>, false, GroupBase<Option<string>>>,
19
+ MappingPickerProps
20
+ >(({ allowClearing, ...rest }, ref) => {
21
+ const stockMappings = getStockNames()
22
+ const userMappings = getUserMappingNames()
23
+
24
+ const options = useMemo(() => {
25
+ const result = allowClearing
26
+ ? [
27
+ {
28
+ value: 'No Mapping',
29
+ label: (
30
+ <span className="zds-mappings-clear-mapping">No Mapping</span>
31
+ ),
32
+ },
33
+ ]
34
+ : []
35
+
36
+ return [
37
+ ...result,
38
+
39
+ {
40
+ label: allowClearing ? <hr /> : '',
41
+ options: userMappings.map(label => ({
42
+ value: label,
43
+ label: <span className="zds-mappings-user-mapping">{label}</span>,
44
+ })),
45
+ },
46
+
47
+ {
48
+ label: userMappings.length ? <hr /> : allowClearing ? <hr /> : '',
49
+ options: stockMappings.map(label => ({
50
+ label,
51
+ value: label,
52
+ })),
53
+ },
54
+ ] as Option<string>[]
55
+ }, [allowClearing, stockMappings, userMappings])
56
+
57
+ return (
58
+ <Select
59
+ {...rest}
60
+ options={options}
61
+ ref={ref}
62
+ />
63
+ )
64
+ })
65
+
66
+ export default MappingPicker
67
+
68
+ export type { MappingPickerProps, MappingPickerRef }