react-native-controlled-input 0.18.0 → 0.20.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.
@@ -5,7 +5,6 @@ import android.util.AttributeSet
5
5
  import android.util.Log
6
6
  import android.util.TypedValue
7
7
  import android.view.View
8
- import android.view.ViewTreeObserver
9
8
  import android.view.inputmethod.InputMethodManager
10
9
  import android.widget.EditText
11
10
  import android.widget.LinearLayout
@@ -27,6 +26,7 @@ import androidx.savedstate.SavedStateRegistryOwner
27
26
  import androidx.savedstate.setViewTreeSavedStateRegistryOwner
28
27
  import com.facebook.react.bridge.ReactContext
29
28
  import com.facebook.react.uimanager.UIManagerHelper
29
+ import com.facebook.react.uimanager.events.Event
30
30
  import kotlinx.coroutines.flow.MutableStateFlow
31
31
 
32
32
  /**
@@ -67,176 +67,184 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
67
67
  private var windowLifecycleBound = false
68
68
 
69
69
  /**
70
- * True while we are in the focus-restoration dance:
71
- * proxy stole Android focus from Compose onBlur fired we're restoring Compose focus.
72
- * During this window the second onFocus must NOT call requestFocusProxy() again.
70
+ * Hidden [EditText] used only as KBC's [FocusedInputObserver.lastFocusedInput]: [syncUpLayout]
71
+ * reads [EditText]-scoped geometry. It is NOT focused and does not participate in the focus
72
+ * chain we push state via reflection + synthetic selection events instead.
73
73
  */
74
- private var isRestoringComposeFocus = false
75
-
76
- /**
77
- * Invisible EditText that acts as a focus proxy for react-native-keyboard-controller.
78
- *
79
- * keyboard-controller's FocusedInputObserver only tracks views that are `EditText` instances.
80
- * Since ControlledInputView uses Compose BasicTextField, it is invisible to that observer.
81
- * Focusing this proxy when Compose gains focus makes keyboard-controller aware of the input
82
- * and allows KeyboardAwareScrollView to scroll correctly.
83
- *
84
- * Layout height is 0 so LinearLayout ignores it visually. onLayout() forces its bounds to
85
- * match ControlledInputView so keyboard-controller reads the correct width/height/position.
86
- */
87
- private val focusProxy: EditText by lazy {
88
- EditText(context).also { proxy ->
89
- proxy.layoutParams = LayoutParams(0, 0)
90
- // Must stay VISIBLE — Android's canTakeFocus() returns false for INVISIBLE/GONE views,
91
- // causing requestFocus() to silently fail. Use alpha=0 to hide it visually instead.
92
- proxy.alpha = 0f
93
- proxy.isFocusableInTouchMode = true
94
- proxy.showSoftInputOnFocus = false
95
- proxy.isClickable = false
96
- proxy.isCursorVisible = false
97
- proxy.isLongClickable = false
98
- }
99
- }
100
-
101
- private fun requestFocusProxy() {
102
- val fontSize = viewModel.inputStyle.value?.fontSize?.toFloat()
103
- Log.d(TAG, "──── requestFocusProxy ────")
104
- Log.d(TAG, " fontSize=$fontSize")
105
- Log.d(TAG, " proxy before: isFocused=${focusProxy.isFocused} size=${focusProxy.width}x${focusProxy.height}")
106
-
107
- // Sync proxy ID with ControlledInputView's React Native tag BEFORE requestFocus(),
108
- // so KeyboardAnimationCallback.focusListener sets viewTagFocused = this.id (not -1).
109
- // Without this, KeyboardAwareScrollView JS sees e.target=-1 → focusWasChanged=false
110
- // → layout.value never updated → maybeScroll() always returns 0 → no scroll.
111
- focusProxy.id = this.id
112
- Log.d(TAG, " proxy.id set to ${focusProxy.id} (= ControlledInputView RN tag)")
113
-
114
- // Sync textSize so KeyboardControllerSelectionWatcher computes the correct
115
- // cursor Y via android.text.Layout.getLineBottom() — used as `customHeight`
116
- // in KeyboardAwareScrollView to determine how far to scroll.
117
- fontSize?.let {
118
- focusProxy.setTextSize(TypedValue.COMPLEX_UNIT_SP, it)
119
- Log.d(TAG, " proxy textSize set to ${focusProxy.textSize}px (${it}sp)")
120
- }
121
-
122
- focusProxy.requestFocus()
123
- Log.d(TAG, " proxy after requestFocus: isFocused=${focusProxy.isFocused} hasFocus=${focusProxy.hasFocus()}")
124
-
125
- if (focusProxy.isFocused) {
126
- // Proxy stole Android focus from AndroidComposeView → Compose will fire onBlur.
127
- // Mark restoration mode so onBlur knows to recover (not dispatch BlurEvent to JS).
128
- isRestoringComposeFocus = true
129
- Log.d(TAG, " isRestoringComposeFocus=true (proxy has Android focus)")
74
+ private val kbcLayoutHost: EditText by lazy {
75
+ EditText(context).also { v ->
76
+ v.layoutParams = LayoutParams(0, 0)
77
+ v.alpha = 0f
78
+ v.isFocusable = false
79
+ v.isFocusableInTouchMode = false
80
+ v.showSoftInputOnFocus = false
81
+ v.isClickable = false
82
+ v.isCursorVisible = false
83
+ v.isLongClickable = false
130
84
  }
131
-
132
- // setSelection triggers a selection change (lastSelectionStart starts at -1),
133
- // guaranteeing the watcher fires on the next pre-draw frame even if focus
134
- // was just transferred.
135
- focusProxy.setSelection(0)
136
- Log.d(TAG, " proxy selectionStart=${focusProxy.selectionStart} layout=${focusProxy.layout != null}")
137
-
138
- // Log absolute screen position — this is what keyboard-controller reads for scroll calculation
139
- val loc = IntArray(2)
140
- focusProxy.getLocationOnScreen(loc)
141
- Log.d(TAG, " proxy screenLocation x=${loc[0]} y=${loc[1]} → absoluteY for KBC=${loc[1]}px")
142
- Log.d(TAG, "──────────────────────────")
143
- }
144
-
145
- private fun clearFocusProxy() {
146
- Log.d(TAG, "clearFocusProxy: proxy.isFocused=${focusProxy.isFocused} hasFocus=${focusProxy.hasFocus()}")
147
- focusProxy.clearFocus()
148
- Log.d(TAG, "clearFocusProxy: after clearFocus isFocused=${focusProxy.isFocused}")
149
85
  }
150
86
 
151
87
  /**
152
- * Fires when the proxy loses Android focus (i.e. after composeView.requestFocus() restores
153
- * Compose). At this point KBC's listener has already cleared lastFocusedInput (KBC registered
154
- * its ViewTreeObserver listener before us → it fires first). We post restoreKbcTracking() to
155
- * run after all synchronous focus-change handlers complete.
156
- */
157
- private val proxyFocusLostListener =
158
- ViewTreeObserver.OnGlobalFocusChangeListener { oldFocus, newFocus ->
159
- // Fire for ANY proxy focus loss while we're in the restoration dance.
160
- // newFocus can be null (proxy→null→AndroidComposeView, two steps on first focus)
161
- // or AndroidComposeView directly (proxy→AndroidComposeView, subsequent focuses).
162
- // Both cases need restoreKbcTracking(); KBC's null→AndroidComposeView transition does
163
- // NOT clear lastFocusedInput, so restoring it once is enough for both paths.
164
- if (oldFocus == focusProxy && isRestoringComposeFocus) {
165
- Log.d(
166
- TAG,
167
- "proxyFocusLostListener: proxy → ${newFocus?.javaClass?.simpleName ?: "null"}, posting restoreKbcTracking()",
168
- )
169
- post { restoreKbcTracking() }
170
- }
171
- }
172
-
173
- /**
174
- * Uses reflection to restore KBC's lastFocusedInput = focusProxy and call syncUpLayout(),
175
- * so that KeyboardAwareScrollView receives the correct absoluteY / height to scroll to.
176
- *
177
- * Chain: EdgeToEdgeViewRegistry.get() → .callback → .layoutObserver → .lastFocusedInput / .syncUpLayout()
88
+ * EdgeToEdgeViewRegistry KeyboardAnimationCallback + FocusedInputObserver.
89
+ * Null if react-native-keyboard-controller is missing or not initialized.
178
90
  */
179
- private fun restoreKbcTracking() {
180
- Log.d(TAG, "restoreKbcTracking: starting reflection chain")
91
+ private fun resolveKbcCallbackAndObserver(): Pair<Any, Any>? {
181
92
  try {
182
- // 1. EdgeToEdgeViewRegistry is a Kotlin object — access via INSTANCE field
183
93
  val registryClass =
184
94
  Class.forName("com.reactnativekeyboardcontroller.views.EdgeToEdgeViewRegistry")
185
95
  val registryInstance = registryClass.getField("INSTANCE").get(null)
186
96
  val edgeToEdgeView =
187
97
  registryClass.getDeclaredMethod("get").invoke(registryInstance)
188
98
  ?: run {
189
- Log.w(TAG, "restoreKbcTracking: EdgeToEdgeViewRegistry.get() == null")
190
- return
99
+ Log.w(TAG, "resolveKbcCallbackAndObserver: EdgeToEdgeViewRegistry.get() == null")
100
+ return null
191
101
  }
192
102
 
193
- // 2. callback: KeyboardAnimationCallback (internal var — find by type)
194
103
  val callbackField =
195
104
  edgeToEdgeView.javaClass.declaredFields.firstOrNull {
196
105
  it.type.simpleName == "KeyboardAnimationCallback"
197
106
  }
198
107
  ?: run {
199
- Log.w(TAG, "restoreKbcTracking: KeyboardAnimationCallback field not found")
200
- return
108
+ Log.w(TAG, "resolveKbcCallbackAndObserver: KeyboardAnimationCallback field not found")
109
+ return null
201
110
  }
202
111
  callbackField.isAccessible = true
203
112
  val callback =
204
113
  callbackField.get(edgeToEdgeView)
205
114
  ?: run {
206
- Log.w(TAG, "restoreKbcTracking: callback == null")
207
- return
115
+ Log.w(TAG, "resolveKbcCallbackAndObserver: callback == null")
116
+ return null
208
117
  }
209
118
 
210
- // 3. layoutObserver: FocusedInputObserver (internal var — find by type)
211
119
  val observerField =
212
120
  callback.javaClass.declaredFields.firstOrNull {
213
121
  it.type.simpleName == "FocusedInputObserver"
214
122
  }
215
123
  ?: run {
216
- Log.w(TAG, "restoreKbcTracking: FocusedInputObserver field not found")
217
- return
124
+ Log.w(TAG, "resolveKbcCallbackAndObserver: FocusedInputObserver field not found")
125
+ return null
218
126
  }
219
127
  observerField.isAccessible = true
220
128
  val observer =
221
129
  observerField.get(callback)
222
130
  ?: run {
223
- Log.w(TAG, "restoreKbcTracking: layoutObserver == null")
131
+ Log.w(TAG, "resolveKbcCallbackAndObserver: layoutObserver == null")
132
+ return null
133
+ }
134
+ return Pair(callback, observer)
135
+ } catch (_: ClassNotFoundException) {
136
+ Log.d(TAG, "resolveKbcCallbackAndObserver: keyboard-controller not on classpath")
137
+ return null
138
+ } catch (e: Exception) {
139
+ Log.w(TAG, "resolveKbcCallbackAndObserver: ${e.javaClass.simpleName}: ${e.message}")
140
+ return null
141
+ }
142
+ }
143
+
144
+ private fun setKbcViewTagFocused(callback: Any) {
145
+ try {
146
+ val f = callback.javaClass.getDeclaredField("viewTagFocused")
147
+ f.isAccessible = true
148
+ f.setInt(callback, id)
149
+ Log.d(TAG, "setKbcViewTagFocused: viewTagFocused=$id")
150
+ } catch (e: Exception) {
151
+ Log.w(TAG, "setKbcViewTagFocused: ${e.javaClass.simpleName}: ${e.message}")
152
+ }
153
+ }
154
+
155
+ private fun setKbcFocusedInputHolder() {
156
+ try {
157
+ val holderClass =
158
+ Class.forName("com.reactnativekeyboardcontroller.traversal.FocusedInputHolder")
159
+ val instance = holderClass.getField("INSTANCE").get(null)
160
+ holderClass
161
+ .getMethod("set", EditText::class.java)
162
+ .invoke(instance, kbcLayoutHost)
163
+ } catch (e: Exception) {
164
+ Log.w(TAG, "setKbcFocusedInputHolder: ${e.javaClass.simpleName}: ${e.message}")
165
+ }
166
+ }
167
+
168
+ /** Approximate one line height in dp for JS customHeight. */
169
+ private fun approximateSelectionEndYDp(): Double {
170
+ viewModel.inputStyle.value?.fontSize?.toDouble()?.takeIf { it > 0 }?.let { return it }
171
+ val dm = resources.displayMetrics
172
+ return (kbcLayoutHost.textSize / dm.density).toDouble().coerceAtLeast(12.0)
173
+ }
174
+
175
+ private fun dispatchSyntheticKbcSelectionEvent(observer: Any) {
176
+ val reactContext = context as? ReactContext ?: return
177
+ try {
178
+ val epField = observer.javaClass.getDeclaredField("eventPropagationView")
179
+ epField.isAccessible = true
180
+ val propagationId = (epField.get(observer) as View).id
181
+
182
+ val surfaceId = UIManagerHelper.getSurfaceId(this)
183
+ val targetId = id
184
+ val endY = approximateSelectionEndYDp()
185
+
186
+ val dataClz =
187
+ Class.forName("com.reactnativekeyboardcontroller.events.FocusedInputSelectionChangedEventData")
188
+ val dataCtor =
189
+ dataClz.declaredConstructors.singleOrNull { it.parameterTypes.size == 7 }
190
+ ?: run {
191
+ Log.w(TAG, "dispatchSyntheticKbcSelectionEvent: no 7-arg data ctor")
224
192
  return
225
193
  }
194
+ dataCtor.isAccessible = true
195
+ val data =
196
+ dataCtor.newInstance(targetId, 0.0, 0.0, 0.0, endY, 0, 0)
197
+
198
+ val eventClz =
199
+ Class.forName("com.reactnativekeyboardcontroller.events.FocusedInputSelectionChangedEvent")
200
+ val eventCtor =
201
+ eventClz.getConstructor(
202
+ Int::class.javaPrimitiveType,
203
+ Int::class.javaPrimitiveType,
204
+ dataClz,
205
+ )
206
+ val event = eventCtor.newInstance(surfaceId, propagationId, data) as Event<*>
207
+
208
+ UIManagerHelper.getEventDispatcherForReactTag(reactContext, propagationId)
209
+ ?.dispatchEvent(event)
210
+ Log.d(
211
+ TAG,
212
+ "dispatchSyntheticKbcSelectionEvent: propagationId=$propagationId target=$targetId endY(dp)=$endY",
213
+ )
214
+ } catch (e: Exception) {
215
+ Log.w(TAG, "dispatchSyntheticKbcSelectionEvent: ${e.javaClass.simpleName}: ${e.message}")
216
+ }
217
+ }
226
218
 
227
- // 4. Set lastFocusedInput = focusProxy (private var)
219
+ /**
220
+ * Pushes ControlledInput state into KBC without stealing Compose focus:
221
+ * viewTagFocused, lastFocusedInput, FocusedInputHolder, syncUpLayout, synthetic selection.
222
+ */
223
+ private fun syncKeyboardControllerFocusedInput() {
224
+ Log.d(TAG, "syncKeyboardControllerFocusedInput: id=$id")
225
+ kbcLayoutHost.id = id
226
+ viewModel.inputStyle.value?.fontSize?.toFloat()?.let {
227
+ kbcLayoutHost.setTextSize(TypedValue.COMPLEX_UNIT_SP, it)
228
+ }
229
+
230
+ val (callback, observer) = resolveKbcCallbackAndObserver() ?: return
231
+
232
+ setKbcViewTagFocused(callback)
233
+ setKbcFocusedInputHolder()
234
+
235
+ try {
228
236
  val lastFocusedField = observer.javaClass.getDeclaredField("lastFocusedInput")
229
237
  lastFocusedField.isAccessible = true
230
- lastFocusedField.set(observer, focusProxy)
231
- Log.d(TAG, "restoreKbcTracking: lastFocusedInput set to focusProxy")
238
+ lastFocusedField.set(observer, kbcLayoutHost)
232
239
 
233
- // 5. Call syncUpLayout() — public fun, dispatches FocusedInputLayoutChangedEvent to JS
234
240
  val syncMethod = observer.javaClass.getDeclaredMethod("syncUpLayout")
235
241
  syncMethod.isAccessible = true
236
242
  syncMethod.invoke(observer)
237
- Log.d(TAG, "restoreKbcTracking: syncUpLayout() invoked — KBC layout event sent to JS")
243
+ Log.d(TAG, "syncKeyboardControllerFocusedInput: syncUpLayout() ok")
244
+
245
+ dispatchSyntheticKbcSelectionEvent(observer)
238
246
  } catch (e: Exception) {
239
- Log.w(TAG, "restoreKbcTracking: ${e.javaClass.simpleName}: ${e.message}")
247
+ Log.w(TAG, "syncKeyboardControllerFocusedInput: ${e.javaClass.simpleName}: ${e.message}")
240
248
  }
241
249
  }
242
250
 
@@ -244,19 +252,15 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
244
252
 
245
253
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
246
254
  super.onLayout(changed, l, t, r, b)
247
- // Force proxy bounds to match ControlledInputView so keyboard-controller reads
248
- // the correct width/height/absolutePosition when the proxy is focused.
249
- focusProxy.layout(0, 0, width, height)
255
+ kbcLayoutHost.layout(0, 0, width, height)
250
256
  if (changed) {
251
257
  val loc = IntArray(2)
252
258
  getLocationOnScreen(loc)
253
259
  Log.d(TAG, "onLayout: view=${width}x${height} screenX=${loc[0]} screenY=${loc[1]}")
254
- Log.d(TAG, "onLayout: proxy=${focusProxy.width}x${focusProxy.height} (should match view)")
255
260
  }
256
261
  }
257
262
 
258
263
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
259
- // Do not measure ComposeView until attached to a window.
260
264
  if (shouldUseAndroidLayout && !isAttachedToWindow) {
261
265
  setMeasuredDimension(
262
266
  MeasureSpec.getSize(widthMeasureSpec).coerceAtLeast(0),
@@ -267,7 +271,6 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
267
271
  super.onMeasure(widthMeasureSpec, heightMeasureSpec)
268
272
  }
269
273
 
270
- // Fabric/Yoga often won't drive Android layout for native children.
271
274
  override fun requestLayout() {
272
275
  super.requestLayout()
273
276
  if (shouldUseAndroidLayout) {
@@ -287,14 +290,11 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
287
290
  override fun onAttachedToWindow() {
288
291
  super.onAttachedToWindow()
289
292
  Log.d(TAG, "onAttachedToWindow: id=$id")
290
- viewTreeObserver.addOnGlobalFocusChangeListener(proxyFocusLostListener)
291
293
  bindComposeToWindowLifecycle()
292
294
  }
293
295
 
294
296
  override fun onDetachedFromWindow() {
295
297
  Log.d(TAG, "onDetachedFromWindow: id=$id")
296
- viewTreeObserver.removeOnGlobalFocusChangeListener(proxyFocusLostListener)
297
- isRestoringComposeFocus = false
298
298
  if (usesLocalFallbackLifecycle) {
299
299
  lifecycleRegistry.currentState = Lifecycle.State.CREATED
300
300
  }
@@ -340,12 +340,10 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
340
340
 
341
341
  fun blur() {
342
342
  Log.d(TAG, "blur() called from JS ref")
343
- isRestoringComposeFocus = false
344
343
  blurSignal.value = blurSignal.value + 1
345
344
  val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
346
345
  imm.hideSoftInputFromWindow(windowToken, 0)
347
346
  clearFocus()
348
- clearFocusProxy()
349
347
  }
350
348
 
351
349
  fun focus() {
@@ -365,7 +363,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
365
363
 
366
364
  viewModel = JetpackComposeViewModel()
367
365
 
368
- addView(focusProxy)
366
+ addView(kbcLayoutHost)
369
367
 
370
368
  composeView = ComposeView(context).also { cv ->
371
369
  cv.layoutParams = LayoutParams(
@@ -417,47 +415,21 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
417
415
  )
418
416
  },
419
417
  onFocus = {
420
- Log.d(TAG, "━━ Compose onFocus ━━ id=$id isRestoring=$isRestoringComposeFocus")
421
- if (isRestoringComposeFocus) {
422
- // Second onFocus triggered by focusRequester.requestFocus() inside the restoration
423
- // dance — KBC is being synced via reflection, keyboard is showing.
424
- // Do NOT dispatch FocusEvent again (already sent on first onFocus).
425
- // Do NOT call requestFocusProxy() again (would cause infinite loop).
426
- Log.d(TAG, " → restoration onFocus: clearing flag, skipping proxy request")
427
- isRestoringComposeFocus = false
428
- } else {
429
- val surfaceId = UIManagerHelper.getSurfaceId(context)
430
- val viewId = this@ControlledInputView.id
431
- UIManagerHelper
432
- .getEventDispatcherForReactTag(context as ReactContext, viewId)
433
- ?.dispatchEvent(FocusEvent(surfaceId, viewId))
434
- requestFocusProxy()
435
- }
418
+ Log.d(TAG, "Compose onFocus id=$id")
419
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
420
+ val viewId = this@ControlledInputView.id
421
+ UIManagerHelper
422
+ .getEventDispatcherForReactTag(context as ReactContext, viewId)
423
+ ?.dispatchEvent(FocusEvent(surfaceId, viewId))
424
+ post { syncKeyboardControllerFocusedInput() }
436
425
  },
437
426
  onBlur = {
438
- Log.d(TAG, "━━ Compose onBlur ━━ id=$id proxyFocused=${focusProxy.isFocused}")
439
- if (focusProxy.isFocused) {
440
- // Proxy stole Android focus from AndroidComposeView → this blur is synthetic.
441
- // Restore Compose focus so the keyboard stays/re-appears.
442
- // Do NOT dispatch BlurEvent to JS.
443
- Log.d(TAG, " → proxy-caused blur: restoring Compose focus")
444
- post {
445
- // Give AndroidComposeView Android focus back (needed for showSoftInput to work)
446
- composeView.requestFocus()
447
- // Trigger BasicTextField to re-gain Compose focus → shows keyboard → fires onFocus
448
- focusSignal.value = focusSignal.value + 1
449
- }
450
- } else {
451
- // Real blur (user dismissed keyboard / tapped elsewhere)
452
- Log.d(TAG, " → real blur: dispatching BlurEvent")
453
- isRestoringComposeFocus = false
454
- val surfaceId = UIManagerHelper.getSurfaceId(context)
455
- val viewId = this@ControlledInputView.id
456
- UIManagerHelper
457
- .getEventDispatcherForReactTag(context as ReactContext, viewId)
458
- ?.dispatchEvent(BlurEvent(surfaceId, viewId))
459
- clearFocusProxy()
460
- }
427
+ Log.d(TAG, "Compose onBlur id=$id")
428
+ val surfaceId = UIManagerHelper.getSurfaceId(context)
429
+ val viewId = this@ControlledInputView.id
430
+ UIManagerHelper
431
+ .getEventDispatcherForReactTag(context as ReactContext, viewId)
432
+ ?.dispatchEvent(BlurEvent(surfaceId, viewId))
461
433
  },
462
434
  focusRequester = focusRequester
463
435
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-controlled-input",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "React Native controlled TextInput with strict value sync (Fabric)",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",