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,13 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ const useStateWithDynamicDefault = <T>(defaultVal: T) => {
4
+ const [state, setState] = useState<T>(defaultVal)
5
+
6
+ useEffect(() => {
7
+ setState(defaultVal)
8
+ }, [defaultVal])
9
+
10
+ return [state, setState] as const
11
+ }
12
+
13
+ export default useStateWithDynamicDefault
package/lib/main.ts ADDED
@@ -0,0 +1,34 @@
1
+ // Pickers
2
+ export { default as CCPicker } from './pickers/CCPicker'
3
+ export { default as ChannelMappingPicker } from './pickers/ChannelMappingPicker'
4
+ export { default as ChannelPicker } from './pickers/ChannelPicker'
5
+ export { default as Knob } from './pickers/Knob'
6
+ export { default as KnobPicker } from './pickers/KnobPicker'
7
+ export { default as LatchPicker } from './pickers/LatchPicker'
8
+ export { default as MappingPicker } from './pickers/MappingPicker'
9
+ export { default as NotePicker } from './pickers/NotePicker'
10
+ export { default as OctavePlayer } from './other/OctavePlayer'
11
+ export { default as PianoPicker } from './pickers/PianoPicker'
12
+ export { default as PolarityPicker } from './pickers/PolarityPicker'
13
+ export { default as ResponseCurvePicker } from './pickers/ResponseCurvePicker'
14
+ export { default as Select } from './pickers/Select'
15
+ export { default as StatusPicker } from './pickers/StatusPicker'
16
+ export { default as SVGText } from './other/SVGText'
17
+ export { default as ValuePicker } from './pickers/ValuePicker'
18
+
19
+ // MIDI
20
+ export {
21
+ combineStatusWithChannel,
22
+ extractStatusAndChannel,
23
+ getStatusLabel,
24
+ MASK_CHANNEL,
25
+ MASK_STATUS,
26
+ Statuses,
27
+ } from './midi/export'
28
+
29
+ export {
30
+ default as ResponseCurve,
31
+ RESPONSE_CURVES,
32
+ } from './pickers/ResponseCurve'
33
+
34
+ export { default as ccValues } from './midi/ccValues'
@@ -0,0 +1,139 @@
1
+ import type { Option } from '../pickers/Select'
2
+
3
+ const CC_NAMES = [
4
+ 'Bank Select',
5
+ 'Modulation Wheel',
6
+ 'Breath Controller',
7
+ '',
8
+ 'Foot Control',
9
+ 'Portamento Time',
10
+ 'Data Entry Slider',
11
+ 'Volume',
12
+ 'Balance',
13
+ '',
14
+ 'Pan',
15
+ 'Expression',
16
+ 'Effect Control 1',
17
+ 'Effect Control 2',
18
+ '',
19
+ '',
20
+ '',
21
+ '',
22
+ '',
23
+ '',
24
+ '',
25
+ '',
26
+ '',
27
+ '',
28
+ '',
29
+ '',
30
+ '',
31
+ '',
32
+ '',
33
+ '',
34
+ '',
35
+ '',
36
+ 'Controller 0',
37
+ 'Controller 1',
38
+ 'Controller 2',
39
+ 'Controller 3',
40
+ 'Controller 4',
41
+ 'Controller 5',
42
+ 'Controller 6',
43
+ 'Controller 7',
44
+ 'Controller 8',
45
+ 'Controller 9',
46
+ 'Controller 10',
47
+ 'Controller 11',
48
+ 'Controller 12',
49
+ 'Controller 13',
50
+ 'Controller 14',
51
+ 'Controller 15',
52
+ 'Controller 16',
53
+ 'Controller 17',
54
+ 'Controller 18',
55
+ 'Controller 19',
56
+ 'Controller 20',
57
+ 'Controller 21',
58
+ 'Controller 22',
59
+ 'Controller 23',
60
+ 'Controller 24',
61
+ 'Controller 25',
62
+ 'Controller 26',
63
+ 'Controller 27',
64
+ 'Controller 28',
65
+ 'Controller 29',
66
+ 'Controller 30',
67
+ 'Controller 31',
68
+ 'Sustain',
69
+ 'Portamento',
70
+ 'Sostenuto',
71
+ 'Soft Pedal',
72
+ 'Legato Pedal',
73
+ 'Sustain 2',
74
+ 'Sound Variation',
75
+ 'Resonance/Timbre',
76
+ 'Sound Release Time',
77
+ 'Sound Attack Time',
78
+ 'Frequency Cutoff',
79
+ 'Sound Control 6',
80
+ 'Sound Control 7',
81
+ 'Sound Control 8',
82
+ 'Sound Control 9',
83
+ 'Sound Control 10',
84
+ 'Decay or General 1',
85
+ 'Hi-Pass or General 2',
86
+ 'General 3',
87
+ 'General 4',
88
+ 'Portamento Amount',
89
+ '',
90
+ '',
91
+ '',
92
+ '',
93
+ '',
94
+ '',
95
+ 'Reverb Level',
96
+ 'Tremolo Depth',
97
+ 'Chorus Level',
98
+ 'Detune',
99
+ 'Phaser Depth',
100
+ 'Data Entry +1',
101
+ 'Data Entry -1',
102
+ 'Non-registered LSB',
103
+ 'Non-registered MSB',
104
+ 'Registered msb',
105
+ 'Registered lsb',
106
+ '',
107
+ '',
108
+ '',
109
+ '',
110
+ '',
111
+ '',
112
+ '',
113
+ '',
114
+ 'Shift 1',
115
+ 'Shift 2',
116
+ 'Shift 3',
117
+ 'Shift 4',
118
+ 'Shift 5',
119
+ 'Shift 6',
120
+ '',
121
+ '',
122
+ '',
123
+ '',
124
+ 'All Sound Off',
125
+ 'All Controllers Off',
126
+ 'Local Mode',
127
+ 'All Notes Off',
128
+ 'Omni Mode Off',
129
+ 'Omni Mode On',
130
+ 'Mono Mode On',
131
+ 'Poly Mode',
132
+ ].map(
133
+ (label, value): Option<number> => ({
134
+ label: String(label).length ? `${value} - ${label}` : String(value),
135
+ value,
136
+ }),
137
+ )
138
+
139
+ export default CC_NAMES
@@ -0,0 +1,54 @@
1
+ import type { Option } from 'lib/pickers/Select'
2
+
3
+ const Statuses = {
4
+ noteOff: 8, // 1000
5
+ noteOn: 9, // 1001
6
+ aftertouch: 10, // 1010
7
+ controlChange: 11, // 1011
8
+ programChange: 12, // 1100
9
+ channelPressure: 13, // 1101
10
+ pitchWheel: 14, // 1110
11
+ }
12
+
13
+ type Status = (typeof Statuses)[keyof typeof Statuses]
14
+
15
+ const MASK_CHANNEL = 15 // 00001111
16
+ const MASK_STATUS = 240 // 01110000
17
+
18
+ const statusOptions: Option<Status>[] = [
19
+ { value: Statuses.noteOn, label: 'Note On' },
20
+ { value: Statuses.noteOff, label: 'Note Stack' },
21
+ { value: Statuses.controlChange, label: 'Control Change' },
22
+ { value: Statuses.programChange, label: 'Program Change' },
23
+ { value: Statuses.aftertouch, label: 'Aftertouch' },
24
+ { value: Statuses.channelPressure, label: 'Channel Pressure' },
25
+ { value: Statuses.pitchWheel, label: 'Pitch Wheel' },
26
+ ]
27
+
28
+ const getStatusLabel = (val: Status) =>
29
+ statusOptions.filter(({ value }) => value === val)[0]?.label || '???'
30
+
31
+ const combineStatusWithChannel = (channel: number, status: Status) =>
32
+ channel | (status << 4)
33
+
34
+ const extractStatusAndChannel = (
35
+ status: number,
36
+ ): {
37
+ status: Status
38
+ channel: number
39
+ } => ({
40
+ status: (((status & MASK_STATUS) >> 4) | 8) as Status,
41
+ channel: status & MASK_CHANNEL,
42
+ })
43
+
44
+ export {
45
+ combineStatusWithChannel,
46
+ extractStatusAndChannel,
47
+ getStatusLabel,
48
+ MASK_CHANNEL,
49
+ MASK_STATUS,
50
+ statusOptions,
51
+ Statuses,
52
+ }
53
+
54
+ export type { Status }
@@ -0,0 +1,14 @@
1
+ import { Fragment, createElement } from 'react'
2
+
3
+ type TooltipProps = {
4
+ children?: React.ReactNode
5
+ placement?: 'left' | 'right' | 'top' | 'bottom'
6
+ title: React.ReactNode
7
+ }
8
+
9
+ const DefaultTooltip = ({ children }: React.PropsWithoutRef<TooltipProps>) =>
10
+ createElement(Fragment, null, children)
11
+
12
+ export default DefaultTooltip
13
+
14
+ export type { TooltipProps }
@@ -0,0 +1,141 @@
1
+ import type Soundfont from 'soundfont-player'
2
+ import { /* Midi, */ Note } from 'tonal'
3
+ import { Piano as ReactPiano } from 'zds-react-piano'
4
+ import type { TooltipProps } from './DefaultTooltip'
5
+ import SoundfontProvider from './SoundFontProvider'
6
+
7
+ const baseNotes = [
8
+ 'C',
9
+ 'C#',
10
+ 'D',
11
+ 'D#',
12
+ 'E',
13
+ 'F',
14
+ 'F#',
15
+ 'G',
16
+ 'G#',
17
+ 'A',
18
+ 'A#',
19
+ 'B',
20
+ ]
21
+
22
+ // const { midiToFreq, midiToNoteName } = Midi
23
+
24
+ // console.log(Note.names(), Note.midi('C4'))
25
+
26
+ // const firstNote = 21
27
+ // const lastNote = 108
28
+ const numNotes = 12
29
+
30
+ const whiteNotes = numNotes * 0.7 // approx
31
+
32
+ const audioContext = new window.AudioContext()
33
+
34
+ type OctavePlayerProps = {
35
+ disabled?: boolean
36
+ height: number
37
+ octave: number
38
+ width: number
39
+ }
40
+
41
+ const OctavePlayer = ({
42
+ octave,
43
+ height,
44
+ width,
45
+ ...rest
46
+ }: OctavePlayerProps) => {
47
+ const keyWidth = width / whiteNotes + 2
48
+ const keyWidthToHeight = keyWidth / height
49
+
50
+ const firstNote = Note.midi(baseNotes[0] + String(octave)) || 0
51
+ const lastNote =
52
+ Note.midi(baseNotes[baseNotes.length - 1] + String(octave)) || 0
53
+
54
+ return (
55
+ <div>
56
+ <ReactPiano
57
+ keyWidthToHeight={keyWidthToHeight}
58
+ noteRange={{ first: firstNote, last: lastNote }}
59
+ width={width}
60
+ {...rest}
61
+ />
62
+ </div>
63
+ )
64
+ }
65
+
66
+ type WithProviderProps = {
67
+ format?: 'mp3' | 'ogg'
68
+ height: number
69
+ hostname?: string
70
+ instrumentName: Soundfont.InstrumentName
71
+ octave?: number
72
+ onChange?: (note: number) => void
73
+ onClick?: (note: number) => void
74
+ onDoubleClick?: (note: number) => void
75
+ onKeyMouseEnter?: (note: number) => void
76
+ onKeyMouseLeave?: (note: number) => void
77
+ soundfont?: 'MusyngKite' | 'FluidR3_GM'
78
+ Tooltip?: TooltipProps
79
+ width: number
80
+ }
81
+
82
+ const WithProvider = ({
83
+ format,
84
+ height,
85
+ hostname,
86
+ instrumentName,
87
+ octave = 4,
88
+ onChange,
89
+ onClick,
90
+ onDoubleClick,
91
+ onKeyMouseEnter,
92
+ onKeyMouseLeave,
93
+ soundfont,
94
+ Tooltip,
95
+ width,
96
+ }: WithProviderProps) => (
97
+ <SoundfontProvider
98
+ {...{
99
+ audioContext,
100
+ format,
101
+ hostname,
102
+ instrumentName,
103
+ onChange,
104
+ soundfont,
105
+ }}
106
+ render={({
107
+ isLoading,
108
+ playNote,
109
+ stopNote,
110
+ }: {
111
+ isLoading: boolean
112
+ playNote: (midiNumber: number) => void
113
+ stopNote: (midiNumber: number) => void
114
+ }) => (
115
+ <>
116
+ <div>
117
+ <h2>Octave: {octave}</h2>
118
+ </div>
119
+ <OctavePlayer
120
+ disabled={isLoading}
121
+ {...{
122
+ height,
123
+ octave,
124
+ onClick,
125
+ onDoubleClick,
126
+ onKeyMouseEnter,
127
+ onKeyMouseLeave,
128
+ playNote,
129
+ stopNote,
130
+ Tooltip,
131
+ width,
132
+ }}
133
+ />
134
+ </>
135
+ )}
136
+ />
137
+ )
138
+
139
+ export default WithProvider
140
+
141
+ export type { WithProviderProps }
@@ -0,0 +1,221 @@
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 Text = (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 default Text
220
+
221
+ export { MEASUREMENT_ELEMENT_ID }