@swmansion/react-native-bottom-sheet 0.9.0-next.3 → 0.9.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.
@@ -17,8 +17,8 @@ import androidx.dynamicanimation.animation.SpringForce
17
17
  import com.facebook.react.bridge.Arguments
18
18
  import com.facebook.react.uimanager.PointerEvents
19
19
  import com.facebook.react.uimanager.StateWrapper
20
- import com.facebook.react.views.view.ReactViewGroup
21
20
  import com.facebook.react.uimanager.events.NativeGestureUtil
21
+ import com.facebook.react.views.view.ReactViewGroup
22
22
  import kotlin.math.abs
23
23
 
24
24
  private enum class DetentKind {
@@ -27,11 +27,14 @@ private enum class DetentKind {
27
27
  }
28
28
 
29
29
  private data class RawDetentSpec(val value: Float, val kind: DetentKind, val programmatic: Boolean)
30
+
30
31
  private data class DetentSpec(val height: Float, val programmatic: Boolean)
31
32
 
32
33
  interface BottomSheetViewListener {
33
34
  fun onIndexChange(index: Int)
35
+
34
36
  fun onSettle(index: Int)
37
+
35
38
  fun onPositionChange(position: Double)
36
39
  }
37
40
 
@@ -54,6 +57,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
54
57
  updateInteractionState()
55
58
  updateScrim()
56
59
  }
60
+
57
61
  var disableScrollableNegotiation: Boolean = false
58
62
  private var pendingIndex: Int? = null
59
63
  private var hasLaidOut = false
@@ -76,15 +80,14 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
76
80
  private var lastTouchY = 0f
77
81
  private var activePointerId = MotionEvent.INVALID_POINTER_ID
78
82
  private var scrimPressed = false
83
+ private var scrimTouchActive = false
79
84
  private var scrimColor = Color.TRANSPARENT
80
85
  private var scrimProgress = 0f
81
86
  private var maxDetentHeight = Float.NaN
82
87
  private var contentHeightMarker: View? = null
83
88
 
84
89
  private val contentHeightMarkerLayoutListener =
85
- View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
86
- refreshDetentsFromLayout()
87
- }
90
+ View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> refreshDetentsFromLayout() }
88
91
 
89
92
  init {
90
93
  clipChildren = false
@@ -198,16 +201,17 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
198
201
  // MARK: - Prop setters
199
202
 
200
203
  fun setDetents(raw: List<Map<String, Any>>) {
201
- rawDetentSpecs = raw.mapNotNull { dict ->
202
- val value = (dict["value"] as? Number)?.toDouble() ?: return@mapNotNull null
203
- val kind =
204
- when ((dict["kind"] as? String)?.lowercase()) {
205
- "content" -> DetentKind.CONTENT
206
- else -> DetentKind.POINTS
207
- }
208
- val programmatic = dict["programmatic"] as? Boolean ?: false
209
- RawDetentSpec(value = (value * density).toFloat(), kind = kind, programmatic = programmatic)
210
- }
204
+ rawDetentSpecs =
205
+ raw.mapNotNull { dict ->
206
+ val value = (dict["value"] as? Number)?.toDouble() ?: return@mapNotNull null
207
+ val kind =
208
+ when ((dict["kind"] as? String)?.lowercase()) {
209
+ "content" -> DetentKind.CONTENT
210
+ else -> DetentKind.POINTS
211
+ }
212
+ val programmatic = dict["programmatic"] as? Boolean ?: false
213
+ RawDetentSpec(value = (value * density).toFloat(), kind = kind, programmatic = programmatic)
214
+ }
211
215
  refreshDetentsFromLayout()
212
216
  }
213
217
 
@@ -323,6 +327,12 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
323
327
  return maxHeight - snapHeight
324
328
  }
325
329
 
330
+ private val minDetentTranslationY: Float
331
+ get() = detentSpecs.indices.minOfOrNull(::translationY) ?: 0f
332
+
333
+ private val maxDetentTranslationY: Float
334
+ get() = detentSpecs.indices.maxOfOrNull(::translationY) ?: 0f
335
+
326
336
  private val closedIndex: Int?
327
337
  get() = detentSpecs.indexOfFirst { it.height == 0f }.takeIf { it >= 0 }
328
338
 
@@ -377,12 +387,13 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
377
387
 
378
388
  private fun startChoreographer() {
379
389
  if (choreographerCallback != null) return
380
- val callback = object : Choreographer.FrameCallback {
381
- override fun doFrame(frameTimeNanos: Long) {
382
- emitPosition()
383
- choreographerCallback?.let { Choreographer.getInstance().postFrameCallback(it) }
390
+ val callback =
391
+ object : Choreographer.FrameCallback {
392
+ override fun doFrame(frameTimeNanos: Long) {
393
+ emitPosition()
394
+ choreographerCallback?.let { Choreographer.getInstance().postFrameCallback(it) }
395
+ }
384
396
  }
385
- }
386
397
  choreographerCallback = callback
387
398
  Choreographer.getInstance().postFrameCallback(callback)
388
399
  }
@@ -407,25 +418,29 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
407
418
  activeAnimationEmitsSettle = emitSettle
408
419
  activeAnimation?.cancel()
409
420
 
410
- val spring = SpringAnimation(sheetContainer, DynamicAnimation.TRANSLATION_Y, targetTy).apply {
411
- spring = SpringForce(targetTy).apply {
412
- dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
413
- stiffness = SpringForce.STIFFNESS_MEDIUM
414
- }
415
- setStartVelocity(velocity)
416
- addEndListener { _, canceled, _, _ ->
417
- if (canceled) {
418
- return@addEndListener
421
+ val spring =
422
+ SpringAnimation(sheetContainer, DynamicAnimation.TRANSLATION_Y, targetTy).apply {
423
+ spring =
424
+ SpringForce(targetTy).apply {
425
+ dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
426
+ stiffness = SpringForce.STIFFNESS_MEDIUM
427
+ }
428
+ setMinValue(minDetentTranslationY)
429
+ setMaxValue(maxDetentTranslationY)
430
+ setStartVelocity(velocity)
431
+ addEndListener { _, canceled, _, _ ->
432
+ if (canceled) {
433
+ return@addEndListener
434
+ }
435
+ stopChoreographer()
436
+ emitPosition()
437
+ activeAnimation = null
438
+ activeAnimationEmitsSettle = false
439
+ updateInteractionState()
440
+ if (emitIndexChange) listener?.onIndexChange(index)
441
+ if (emitSettle) listener?.onSettle(index)
419
442
  }
420
- stopChoreographer()
421
- emitPosition()
422
- activeAnimation = null
423
- activeAnimationEmitsSettle = false
424
- updateInteractionState()
425
- if (emitIndexChange) listener?.onIndexChange(index)
426
- if (emitSettle) listener?.onSettle(index)
427
443
  }
428
- }
429
444
 
430
445
  activeAnimation = spring
431
446
  startChoreographer()
@@ -440,11 +455,13 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
440
455
 
441
456
  if (velocity < -flickThreshold) {
442
457
  return draggable.firstOrNull { it.value.height > currentHeight }?.index
443
- ?: draggable.lastOrNull()?.index ?: targetIndex
458
+ ?: draggable.lastOrNull()?.index
459
+ ?: targetIndex
444
460
  }
445
461
  if (velocity > flickThreshold) {
446
462
  return draggable.lastOrNull { it.value.height < currentHeight }?.index
447
- ?: draggable.firstOrNull()?.index ?: targetIndex
463
+ ?: draggable.firstOrNull()?.index
464
+ ?: targetIndex
448
465
  }
449
466
 
450
467
  return draggable.minByOrNull { abs(it.value.height - currentHeight) }?.index ?: targetIndex
@@ -459,7 +476,9 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
459
476
  initialTouchX = ev.x
460
477
  initialTouchY = ev.y
461
478
  lastTouchY = ev.y
479
+ activePointerId = ev.getPointerId(0)
462
480
  scrimPressed = true
481
+ scrimTouchActive = true
463
482
  return true
464
483
  }
465
484
  return false
@@ -503,18 +522,20 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
503
522
  }
504
523
  }
505
524
  }
506
- MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
525
+ MotionEvent.ACTION_UP,
526
+ MotionEvent.ACTION_CANCEL -> {
507
527
  initialTouchX = 0f
508
528
  initialTouchY = 0f
509
529
  activePointerId = MotionEvent.INVALID_POINTER_ID
510
530
  scrimPressed = false
531
+ scrimTouchActive = false
511
532
  }
512
533
  }
513
534
  return false
514
535
  }
515
536
 
516
537
  override fun onTouchEvent(event: MotionEvent): Boolean {
517
- if (scrimPressed) {
538
+ if (scrimTouchActive) {
518
539
  when (event.actionMasked) {
519
540
  MotionEvent.ACTION_MOVE -> {
520
541
  val sheetTop = sheetContainer.top + sheetContainer.translationY
@@ -525,14 +546,22 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
525
546
  }
526
547
  MotionEvent.ACTION_UP -> {
527
548
  val closeIndex = closedIndex
549
+ val shouldDismiss = scrimPressed && isScrimVisible()
528
550
  scrimPressed = false
529
- if (closeIndex != null && isScrimVisible()) {
551
+ scrimTouchActive = false
552
+ activePointerId = MotionEvent.INVALID_POINTER_ID
553
+ if (shouldDismiss && closeIndex != null) {
530
554
  snapToIndex(closeIndex, 0f)
531
555
  }
532
556
  return true
533
557
  }
534
558
  MotionEvent.ACTION_CANCEL -> {
535
559
  scrimPressed = false
560
+ scrimTouchActive = false
561
+ activePointerId = MotionEvent.INVALID_POINTER_ID
562
+ return true
563
+ }
564
+ MotionEvent.ACTION_POINTER_UP -> {
536
565
  return true
537
566
  }
538
567
  }
@@ -553,15 +582,17 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
553
582
  emitPosition()
554
583
  return true
555
584
  }
556
- MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
585
+ MotionEvent.ACTION_UP,
586
+ MotionEvent.ACTION_CANCEL -> {
557
587
  isPanning = false
558
588
  activePointerId = MotionEvent.INVALID_POINTER_ID
559
- val velocity = velocityTracker?.let { tracker ->
560
- tracker.computeCurrentVelocity(1000)
561
- val v = tracker.yVelocity
562
- tracker.recycle()
563
- v
564
- } ?: 0f
589
+ val velocity =
590
+ velocityTracker?.let { tracker ->
591
+ tracker.computeCurrentVelocity(1000)
592
+ val v = tracker.yVelocity
593
+ tracker.recycle()
594
+ v
595
+ } ?: 0f
565
596
  velocityTracker = null
566
597
  val maxHeight = detentSpecs.maxOfOrNull { it.height } ?: resolvedMaxDetentHeight()
567
598
  val currentHeight = maxHeight - sheetContainer.translationY
@@ -616,9 +647,9 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
616
647
  val containerY = initialTouchY - sheetContainer.top - sheetContainer.translationY
617
648
  if (
618
649
  containerX < 0f ||
619
- containerX >= sheetContainer.width ||
620
- containerY < 0f ||
621
- containerY >= sheetContainer.height
650
+ containerX >= sheetContainer.width ||
651
+ containerY < 0f ||
652
+ containerY >= sheetContainer.height
622
653
  ) {
623
654
  return null
624
655
  }
@@ -636,7 +667,9 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
636
667
  if (childX < 0f || childX >= child.width || childY < 0f || childY >= child.height) {
637
668
  continue
638
669
  }
639
- findScrollableAtPoint(child, childX, childY)?.let { return it }
670
+ findScrollableAtPoint(child, childX, childY)?.let {
671
+ return it
672
+ }
640
673
  }
641
674
  }
642
675
 
@@ -680,6 +713,7 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
680
713
  lastTouchY = 0f
681
714
  activePointerId = MotionEvent.INVALID_POINTER_ID
682
715
  scrimPressed = false
716
+ scrimTouchActive = false
683
717
  sheetContainer.translationY = 0f
684
718
  scrimProgress = 0f
685
719
  sheetContainer.removeAllViews()
@@ -696,15 +730,16 @@ class BottomSheetView(context: Context) : ReactViewGroup(context) {
696
730
 
697
731
  // When settled at the closed detent, dynamic content updates can briefly
698
732
  // produce stale non-zero positions. Keep scrim hidden in this state.
699
- if (closedIndex != null && targetIndex == closedIndex && activeAnimation == null && !isPanning) {
733
+ if (
734
+ closedIndex != null && targetIndex == closedIndex && activeAnimation == null && !isPanning
735
+ ) {
700
736
  scrimProgress = 0f
701
737
  invalidate()
702
738
  return
703
739
  }
704
740
 
705
741
  val threshold = firstNonZeroDetentHeight
706
- scrimProgress =
707
- if (threshold <= 0f) 0f else (position / threshold).coerceIn(0f, 1f)
742
+ scrimProgress = if (threshold <= 0f) 0f else (position / threshold).coerceIn(0f, 1f)
708
743
  invalidate()
709
744
  }
710
745
 
@@ -14,8 +14,7 @@ import com.facebook.react.viewmanagers.BottomSheetViewManagerInterface
14
14
 
15
15
  @ReactModule(name = BottomSheetViewManager.NAME)
16
16
  class BottomSheetViewManager :
17
- ViewGroupManager<BottomSheetView>(),
18
- BottomSheetViewManagerInterface<BottomSheetView> {
17
+ ViewGroupManager<BottomSheetView>(), BottomSheetViewManagerInterface<BottomSheetView> {
19
18
 
20
19
  companion object {
21
20
  const val NAME = "BottomSheetView"
@@ -29,37 +28,37 @@ class BottomSheetViewManager :
29
28
 
30
29
  override fun createViewInstance(context: ThemedReactContext): BottomSheetView {
31
30
  val view = BottomSheetView(context)
32
- view.listener = object : BottomSheetViewListener {
33
- override fun onIndexChange(index: Int) {
34
- val event = com.facebook.react.bridge.Arguments.createMap().apply {
35
- putInt("index", index)
31
+ view.listener =
32
+ object : BottomSheetViewListener {
33
+ override fun onIndexChange(index: Int) {
34
+ val event =
35
+ com.facebook.react.bridge.Arguments.createMap().apply { putInt("index", index) }
36
+ val reactContext = view.context as? ThemedReactContext ?: return
37
+ reactContext
38
+ .getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
39
+ .receiveEvent(view.id, "topIndexChange", event)
36
40
  }
37
- val reactContext = view.context as? ThemedReactContext ?: return
38
- reactContext
39
- .getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
40
- .receiveEvent(view.id, "topIndexChange", event)
41
- }
42
41
 
43
- override fun onSettle(index: Int) {
44
- val event = com.facebook.react.bridge.Arguments.createMap().apply {
45
- putInt("index", index)
42
+ override fun onSettle(index: Int) {
43
+ val event =
44
+ com.facebook.react.bridge.Arguments.createMap().apply { putInt("index", index) }
45
+ val reactContext = view.context as? ThemedReactContext ?: return
46
+ reactContext
47
+ .getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
48
+ .receiveEvent(view.id, "topSettle", event)
46
49
  }
47
- val reactContext = view.context as? ThemedReactContext ?: return
48
- reactContext
49
- .getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
50
- .receiveEvent(view.id, "topSettle", event)
51
- }
52
50
 
53
- override fun onPositionChange(position: Double) {
54
- val event = com.facebook.react.bridge.Arguments.createMap().apply {
55
- putDouble("position", position)
51
+ override fun onPositionChange(position: Double) {
52
+ val event =
53
+ com.facebook.react.bridge.Arguments.createMap().apply {
54
+ putDouble("position", position)
55
+ }
56
+ val reactContext = view.context as? ThemedReactContext ?: return
57
+ reactContext
58
+ .getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
59
+ .receiveEvent(view.id, "topPositionChange", event)
56
60
  }
57
- val reactContext = view.context as? ThemedReactContext ?: return
58
- reactContext
59
- .getJSModule(com.facebook.react.uimanager.events.RCTEventEmitter::class.java)
60
- .receiveEvent(view.id, "topPositionChange", event)
61
61
  }
62
- }
63
62
  return view
64
63
  }
65
64
 
@@ -69,7 +68,8 @@ class BottomSheetViewManager :
69
68
 
70
69
  override fun getChildCount(parent: BottomSheetView): Int = parent.sheetChildCount
71
70
 
72
- override fun getChildAt(parent: BottomSheetView, index: Int): View? = parent.getSheetChildAt(index)
71
+ override fun getChildAt(parent: BottomSheetView, index: Int): View? =
72
+ parent.getSheetChildAt(index)
73
73
 
74
74
  override fun removeViewAt(parent: BottomSheetView, index: Int) {
75
75
  parent.removeSheetChildAt(index)
@@ -28,12 +28,15 @@ public final class RNSBottomSheetHostingView: UIView {
28
28
  public var modal: Bool = false {
29
29
  didSet { updateScrim() }
30
30
  }
31
+
31
32
  public var scrimColor: UIColor? = .clear {
32
33
  didSet { scrimView.backgroundColor = scrimColor }
33
34
  }
35
+
34
36
  public var maxDetentHeight: CGFloat = .nan {
35
37
  didSet { refreshDetentsFromLayout() }
36
38
  }
39
+
37
40
  public var disableScrollableNegotiation: Bool = false
38
41
 
39
42
  private var rawDetentSpecs: [RawDetentSpec] = []
@@ -59,7 +62,7 @@ public final class RNSBottomSheetHostingView: UIView {
59
62
  private var isContentInteractionDisabled = false
60
63
  private weak var contentHeightMarker: UIView?
61
64
 
62
- public override init(frame: CGRect) {
65
+ override public init(frame: CGRect) {
63
66
  super.init(frame: frame)
64
67
  backgroundColor = .clear
65
68
  clipsToBounds = false
@@ -84,19 +87,20 @@ public final class RNSBottomSheetHostingView: UIView {
84
87
  sheetContainer.addGestureRecognizer(panGesture)
85
88
  }
86
89
 
87
- public required init?(coder: NSCoder) {
90
+ @available(*, unavailable)
91
+ public required init?(coder _: NSCoder) {
88
92
  fatalError("init(coder:) has not been implemented")
89
93
  }
90
94
 
91
- // RCTSurfaceTouchHandler dispatches touch events to JS independently of the
92
- // pan gesture (it fires in touchesBegan: regardless of its recognizer state).
93
- // We cache it here and toggle isEnabled in handlePan(.began) to force a
94
- // touchesCancelled dispatch to JS, preventing Pressable from firing onPress
95
- // during a sheet drag. This is the iOS equivalent of Android's
96
- // NativeGestureUtil.notifyNativeGestureStarted.
95
+ /// RCTSurfaceTouchHandler dispatches touch events to JS independently of the
96
+ /// pan gesture (it fires in touchesBegan: regardless of its recognizer state).
97
+ /// We cache it here and toggle isEnabled in handlePan(.began) to force a
98
+ /// touchesCancelled dispatch to JS, preventing Pressable from firing onPress
99
+ /// during a sheet drag. This is the iOS equivalent of Android's
100
+ /// NativeGestureUtil.notifyNativeGestureStarted.
97
101
  private weak var surfaceTouchHandler: UIGestureRecognizer?
98
102
 
99
- public override func didMoveToWindow() {
103
+ override public func didMoveToWindow() {
100
104
  super.didMoveToWindow()
101
105
  surfaceTouchHandler = nil
102
106
  guard window != nil else { return }
@@ -112,7 +116,7 @@ public final class RNSBottomSheetHostingView: UIView {
112
116
  }
113
117
  }
114
118
 
115
- public override func layoutSubviews() {
119
+ override public func layoutSubviews() {
116
120
  super.layoutSubviews()
117
121
  guard bounds.width > 0, bounds.height > 0 else { return }
118
122
 
@@ -152,7 +156,7 @@ public final class RNSBottomSheetHostingView: UIView {
152
156
  return sheetContainer.frame
153
157
  }
154
158
 
155
- public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
159
+ override public func point(inside point: CGPoint, with _: UIEvent?) -> Bool {
156
160
  if presentedSheetFrame.contains(point) {
157
161
  return true
158
162
  }
@@ -160,10 +164,10 @@ public final class RNSBottomSheetHostingView: UIView {
160
164
  return isScrimVisible && bounds.contains(point)
161
165
  }
162
166
 
163
- public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
167
+ override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
164
168
  guard self.point(inside: point, with: event) else { return nil }
165
169
 
166
- if isScrimVisible && !presentedSheetFrame.contains(point) {
170
+ if isScrimVisible, !presentedSheetFrame.contains(point) {
167
171
  let scrimPoint = convert(point, to: scrimView)
168
172
  return scrimView.hitTest(scrimPoint, with: event)
169
173
  }
@@ -475,7 +479,7 @@ public final class RNSBottomSheetHostingView: UIView {
475
479
  return nil
476
480
  }
477
481
 
478
- public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
482
+ override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
479
483
  guard gestureRecognizer === panGesture else { return true }
480
484
 
481
485
  let velocity = panGesture.velocity(in: self)
@@ -552,7 +556,7 @@ public final class RNSBottomSheetHostingView: UIView {
552
556
  return
553
557
  }
554
558
 
555
- if hasLaidOut && !isPanning {
559
+ if hasLaidOut, !isPanning {
556
560
  targetIndex = max(0, min(detentSpecs.count - 1, targetIndex))
557
561
 
558
562
  if let animator = activeAnimator {
@@ -606,8 +610,8 @@ extension RNSBottomSheetHostingView: UIGestureRecognizerDelegate {
606
610
  }
607
611
 
608
612
  public func gestureRecognizer(
609
- _ gestureRecognizer: UIGestureRecognizer,
610
- shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer
613
+ _: UIGestureRecognizer,
614
+ shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer
611
615
  ) -> Bool {
612
616
  return false
613
617
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/react-native-bottom-sheet",
3
- "version": "0.9.0-next.3",
3
+ "version": "0.9.0",
4
4
  "description": "Provides bottom-sheet components for React Native.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -35,7 +35,7 @@
35
35
  "scripts": {
36
36
  "example": "yarn workspace @swmansion/react-native-bottom-sheet-example",
37
37
  "clean": "del-cli lib",
38
- "prepare": "bob build",
38
+ "prepare": "lefthook install && bob build",
39
39
  "typecheck": "tsc",
40
40
  "lint": "eslint \"**/*.{js,ts,tsx}\""
41
41
  },
@@ -69,6 +69,7 @@
69
69
  "eslint": "9.39.2",
70
70
  "eslint-config-prettier": "10.1.8",
71
71
  "eslint-plugin-prettier": "5.5.5",
72
+ "lefthook": "2.1.6",
72
73
  "prettier": "2.8.8",
73
74
  "react": "19.1.0",
74
75
  "react-native": "0.81.5",