clox-picker 0.1.1 → 0.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.
@@ -24,6 +24,9 @@ class CloxPickerModule : Module() {
24
24
  Prop("useLiquidGlass") { view: CloxPickerView, _: Boolean ->
25
25
  // Android: no-op (always non–liquid-glass look)
26
26
  }
27
+ Prop("selectedColor") { view: CloxPickerView, color: String? ->
28
+ view.setSelectedColor(color)
29
+ }
27
30
  Events("onTabChange")
28
31
  }
29
32
  }
@@ -2,26 +2,38 @@ package expo.modules.cloxpicker
2
2
 
3
3
  import android.animation.ValueAnimator
4
4
  import android.content.Context
5
+ import android.graphics.Bitmap
6
+ import android.graphics.BitmapFactory
7
+ import android.graphics.Color
8
+ import android.graphics.PorterDuff
5
9
  import android.graphics.drawable.GradientDrawable
10
+ import android.os.Handler
11
+ import android.os.Looper
6
12
  import android.util.TypedValue
7
13
  import android.view.Gravity
8
14
  import android.view.View
9
- import android.widget.FrameLayout
15
+ import android.widget.ImageView
10
16
  import android.widget.LinearLayout
11
17
  import android.widget.TextView
12
18
  import expo.modules.kotlin.AppContext
13
19
  import expo.modules.kotlin.viewevent.EventDispatcher
14
20
  import expo.modules.kotlin.views.ExpoView
21
+ import java.io.File
22
+ import java.io.InputStream
23
+ import java.net.HttpURLConnection
24
+ import java.net.URL
25
+ import java.util.concurrent.Executors
15
26
 
16
- data class TabItem(val id: Int, val name: String, val iconUri: String?)
27
+ data class TabItem(val id: Int, val name: String, val iconUri: String?, val iconWidth: Int?, val iconHeight: Int?)
17
28
 
18
29
  class CloxPickerView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
19
30
 
20
31
  private val onTabChange by EventDispatcher()
21
32
 
22
- private var tabs: List<TabItem> = listOf(TabItem(0, "Tab", null))
33
+ private var tabs: List<TabItem> = listOf(TabItem(0, "Tab", null, null, null))
23
34
  private var pickerHeight: Int = dp(44)
24
35
  private var selectedIndex: Int = 0
36
+ private var selectedColor: Int = 0xFF007AFF.toInt() // Default iOS blue
25
37
  private var thumbWidth: Int = 0
26
38
  private var thumbHeight: Int = 0
27
39
  private var thumbTop: Int = 0
@@ -29,20 +41,33 @@ class CloxPickerView(context: Context, appContext: AppContext) : ExpoView(contex
29
41
  private val trackBackground: View
30
42
  private val thumb: View
31
43
  private val segmentsContainer: LinearLayout
44
+
45
+ private val executor = Executors.newCachedThreadPool()
46
+ private val mainHandler = Handler(Looper.getMainLooper())
32
47
 
33
48
  init {
34
49
  setBackgroundColor(0)
35
50
 
51
+ // Track background (pill-shaped)
36
52
  trackBackground = View(context).apply {
37
- setBackgroundColor(0xFFEBEBEB.toInt())
53
+ background = GradientDrawable().apply {
54
+ setColor(0xFFEBEBEB.toInt())
55
+ cornerRadius = (pickerHeight / 2).toFloat()
56
+ }
38
57
  }
39
58
  addView(trackBackground)
40
59
 
60
+ // Thumb (selected indicator)
41
61
  thumb = View(context).apply {
42
- setBackgroundColor(0xFFFFFFFF.toInt() and 0x00FFFFFF or (0.65 * 255).toInt().shl(24))
62
+ background = GradientDrawable().apply {
63
+ setColor(0xFFFFFFFF.toInt())
64
+ cornerRadius = (pickerHeight / 2).toFloat()
65
+ }
66
+ elevation = 2f // Material Design elevation
43
67
  }
44
68
  addView(thumb)
45
69
 
70
+ // Segments container (text + icons)
46
71
  segmentsContainer = LinearLayout(context).apply {
47
72
  orientation = LinearLayout.HORIZONTAL
48
73
  gravity = Gravity.CENTER_VERTICAL
@@ -54,14 +79,55 @@ class CloxPickerView(context: Context, appContext: AppContext) : ExpoView(contex
54
79
  return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value.toFloat(), resources.displayMetrics).toInt()
55
80
  }
56
81
 
82
+ private fun parseColor(colorString: String?): Int? {
83
+ if (colorString == null) return null
84
+
85
+ return try {
86
+ when (colorString.lowercase()) {
87
+ "red" -> 0xFFFF3B30.toInt()
88
+ "blue" -> 0xFF007AFF.toInt()
89
+ "green" -> 0xFF34C759.toInt()
90
+ "orange" -> 0xFFFF9500.toInt()
91
+ "purple" -> 0xFFAF52DE.toInt()
92
+ "pink" -> 0xFFFF2D55.toInt()
93
+ "yellow" -> 0xFFFFCC00.toInt()
94
+ "teal" -> 0xFF5AC8FA.toInt()
95
+ "indigo" -> 0xFF5856D6.toInt()
96
+ else -> {
97
+ // Try parsing as hex
98
+ var hex = colorString.trim().replace("#", "")
99
+ if (hex.length == 6) {
100
+ Color.parseColor("#$hex")
101
+ } else if (hex.length == 8) {
102
+ // ARGB format
103
+ val alpha = hex.substring(0, 2).toInt(16)
104
+ val rgb = hex.substring(2, 8)
105
+ Color.parseColor("#$rgb").let { Color.argb(alpha, Color.red(it), Color.green(it), Color.blue(it)) }
106
+ } else {
107
+ Color.parseColor(colorString)
108
+ }
109
+ }
110
+ }
111
+ } catch (e: Exception) {
112
+ null
113
+ }
114
+ }
115
+
57
116
  fun setTabs(tabsList: List<Map<String, Any>>) {
58
117
  tabs = tabsList.mapNotNull { dict ->
59
118
  val name = dict["name"] as? String ?: return@mapNotNull null
60
119
  val id = (dict["id"] as? Number)?.toInt() ?: return@mapNotNull null
61
- val icon = (dict["icon"] as? Map<*, *>)?.get("uri") as? String
62
- TabItem(id, name, icon)
120
+ val iconDict = dict["icon"] as? Map<*, *>
121
+ val iconUri = iconDict?.get("uri") as? String
122
+ val iconWidth = (iconDict?.get("width") as? Number)?.toInt()
123
+ val iconHeight = (iconDict?.get("height") as? Number)?.toInt()
124
+ TabItem(id, name, iconUri, iconWidth, iconHeight)
125
+ }
126
+ if (tabs.isEmpty()) tabs = listOf(TabItem(0, "Tab", null, null, null))
127
+ // Re-validate selectedIndex against the new tabs count
128
+ if (tabs.isNotEmpty()) {
129
+ selectedIndex = selectedIndex.coerceIn(0, tabs.size - 1)
63
130
  }
64
- if (tabs.isEmpty()) tabs = listOf(TabItem(0, "Tab", null))
65
131
  buildSegments()
66
132
  }
67
133
 
@@ -71,25 +137,91 @@ class CloxPickerView(context: Context, appContext: AppContext) : ExpoView(contex
71
137
  }
72
138
 
73
139
  fun setValue(value: Int) {
74
- val newIndex = value.coerceIn(0, (tabs.size - 1).coerceAtLeast(0))
140
+ // If tabs haven't been set yet, store the value as-is (will be validated when tabs are set)
141
+ // Otherwise, clamp to valid range
142
+ val newIndex = if (tabs.isEmpty()) {
143
+ value
144
+ } else {
145
+ value.coerceIn(0, tabs.size - 1)
146
+ }
147
+
75
148
  if (newIndex != selectedIndex) {
76
149
  selectedIndex = newIndex
77
150
  animateThumbTo(selectedIndex)
78
151
  buildSegments()
152
+ requestLayout()
153
+ }
154
+ }
155
+
156
+ fun setSelectedColor(colorString: String?) {
157
+ val color = parseColor(colorString) ?: 0xFF007AFF.toInt()
158
+ if (selectedColor != color) {
159
+ selectedColor = color
160
+ updateThumbColor()
161
+ buildSegments()
162
+ }
163
+ }
164
+
165
+ private fun updateThumbColor() {
166
+ (thumb.background as? GradientDrawable)?.setColor(selectedColor)
167
+ thumb.invalidate()
168
+ }
169
+
170
+ private fun loadImage(uri: String?, callback: (Bitmap?) -> Unit) {
171
+ if (uri == null) {
172
+ callback(null)
173
+ return
174
+ }
175
+
176
+ executor.execute {
177
+ try {
178
+ val bitmap = when {
179
+ uri.startsWith("http://") || uri.startsWith("https://") -> {
180
+ // Load from URL
181
+ val url = URL(uri)
182
+ val connection = url.openConnection() as HttpURLConnection
183
+ connection.connectTimeout = 5000
184
+ connection.readTimeout = 5000
185
+ connection.doInput = true
186
+ connection.connect()
187
+
188
+ val inputStream: InputStream = connection.inputStream
189
+ val bitmap = BitmapFactory.decodeStream(inputStream)
190
+ inputStream.close()
191
+ connection.disconnect()
192
+ bitmap
193
+ }
194
+ uri.startsWith("/") -> {
195
+ // Local file
196
+ BitmapFactory.decodeFile(uri)
197
+ }
198
+ else -> null
199
+ }
200
+
201
+ mainHandler.post {
202
+ callback(bitmap)
203
+ }
204
+ } catch (e: Exception) {
205
+ mainHandler.post {
206
+ callback(null)
207
+ }
208
+ }
79
209
  }
80
210
  }
81
211
 
82
212
  private fun buildSegments() {
83
213
  segmentsContainer.removeAllViews()
84
214
  val count = tabs.size.coerceAtLeast(1)
215
+ val iconMargin = dp(4)
216
+
85
217
  for ((index, tab) in tabs.withIndex()) {
86
- val textView = TextView(context).apply {
87
- text = tab.name
88
- setTextSize(TypedValue.COMPLEX_UNIT_SP, 15f)
89
- setTextColor(if (index == selectedIndex) 0xFF000000.toInt() else 0xFF8E8E93.toInt())
90
- setTypeface(null, if (index == selectedIndex) android.graphics.Typeface.BOLD else android.graphics.Typeface.NORMAL)
218
+ val isSelected = index == selectedIndex
219
+
220
+ // Container for icon + text
221
+ val segmentContainer = LinearLayout(context).apply {
222
+ orientation = LinearLayout.VERTICAL
91
223
  gravity = Gravity.CENTER
92
- layoutParams = LinearLayout.LayoutParams(0, pickerHeight, 1f)
224
+ layoutParams = LinearLayout.LayoutParams(0, pickerHeight, 1f)
93
225
  setOnClickListener {
94
226
  if (index != selectedIndex) {
95
227
  selectedIndex = index
@@ -99,7 +231,56 @@ class CloxPickerView(context: Context, appContext: AppContext) : ExpoView(contex
99
231
  }
100
232
  }
101
233
  }
102
- segmentsContainer.addView(textView)
234
+
235
+ // Icon (if provided)
236
+ if (tab.iconUri != null) {
237
+ val iconWidth = tab.iconWidth?.let { dp(it) } ?: dp(20)
238
+ val iconHeight = tab.iconHeight?.let { dp(it) } ?: dp(20)
239
+
240
+ val iconView = ImageView(context).apply {
241
+ layoutParams = LinearLayout.LayoutParams(iconWidth, iconHeight).apply {
242
+ gravity = Gravity.CENTER_HORIZONTAL
243
+ bottomMargin = iconMargin
244
+ }
245
+ scaleType = ImageView.ScaleType.FIT_CENTER
246
+ // Set tint color based on selection
247
+ setColorFilter(
248
+ if (isSelected) selectedColor else 0xFF8E8E93.toInt(),
249
+ PorterDuff.Mode.SRC_IN
250
+ )
251
+ }
252
+
253
+ loadImage(tab.iconUri) { bitmap ->
254
+ if (bitmap != null) {
255
+ iconView.setImageBitmap(bitmap)
256
+ // Reapply color filter after image loads
257
+ iconView.setColorFilter(
258
+ if (isSelected) selectedColor else 0xFF8E8E93.toInt(),
259
+ PorterDuff.Mode.SRC_IN
260
+ )
261
+ }
262
+ }
263
+
264
+ segmentContainer.addView(iconView)
265
+ }
266
+
267
+ // Text label
268
+ val textView = TextView(context).apply {
269
+ text = tab.name
270
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
271
+ setTextColor(if (isSelected) selectedColor else 0xFF8E8E93.toInt())
272
+ setTypeface(null, if (isSelected) android.graphics.Typeface.BOLD else android.graphics.Typeface.NORMAL)
273
+ gravity = Gravity.CENTER
274
+ layoutParams = LinearLayout.LayoutParams(
275
+ LinearLayout.LayoutParams.WRAP_CONTENT,
276
+ LinearLayout.LayoutParams.WRAP_CONTENT
277
+ ).apply {
278
+ gravity = Gravity.CENTER_HORIZONTAL
279
+ }
280
+ }
281
+ segmentContainer.addView(textView)
282
+
283
+ segmentsContainer.addView(segmentContainer)
103
284
  }
104
285
  }
105
286
 
@@ -138,24 +319,32 @@ class CloxPickerView(context: Context, appContext: AppContext) : ExpoView(contex
138
319
  val thumbHeight = pickerHeight - padding * 2
139
320
  val topOffset = (h - pickerHeight) / 2
140
321
 
141
- // Track (pill background)
142
- (trackBackground.background as? GradientDrawable)?.cornerRadius = (pickerHeight / 2).toFloat()
322
+ // Track (pill background) - adapt to dark/light mode
323
+ val trackColor = if (isDarkMode()) 0xFF2C2C2E.toInt() else 0xFFEBEBEB.toInt()
324
+ (trackBackground.background as? GradientDrawable)?.apply {
325
+ setColor(trackColor)
326
+ cornerRadius = (pickerHeight / 2).toFloat()
327
+ }
143
328
  if (trackBackground.background !is GradientDrawable) {
144
329
  trackBackground.background = GradientDrawable().apply {
145
- setColor(0xFFEBEBEB.toInt())
330
+ setColor(trackColor)
146
331
  cornerRadius = (pickerHeight / 2).toFloat()
147
332
  }
148
333
  }
149
334
  trackBackground.layout(0, topOffset, w, topOffset + pickerHeight)
150
335
 
151
- // Thumb (rounded rect)
336
+ // Thumb (rounded rect with selected color)
152
337
  this.thumbWidth = thumbWidth
153
338
  this.thumbHeight = thumbHeight
154
339
  this.thumbTop = topOffset + padding
155
340
  val thumbLeft = padding + selectedIndex * segmentWidth + (segmentWidth - thumbWidth) / 2
341
+ (thumb.background as? GradientDrawable)?.apply {
342
+ setColor(selectedColor)
343
+ cornerRadius = (thumbHeight / 2).toFloat()
344
+ }
156
345
  if (thumb.background !is GradientDrawable) {
157
346
  thumb.background = GradientDrawable().apply {
158
- setColor(0xFFFFFFFF.toInt() and 0x00FFFFFF or (0.65 * 255).toInt().shl(24))
347
+ setColor(selectedColor)
159
348
  cornerRadius = (thumbHeight / 2).toFloat()
160
349
  }
161
350
  }
@@ -165,6 +354,11 @@ class CloxPickerView(context: Context, appContext: AppContext) : ExpoView(contex
165
354
  segmentsContainer.layout(0, topOffset, w, topOffset + pickerHeight)
166
355
  }
167
356
 
357
+ private fun isDarkMode(): Boolean {
358
+ val nightModeFlags = resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK
359
+ return nightModeFlags == android.content.res.Configuration.UI_MODE_NIGHT_YES
360
+ }
361
+
168
362
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
169
363
  val h = View.resolveSize(pickerHeight, heightMeasureSpec)
170
364
  val w = View.getDefaultSize(suggestedMinimumWidth, widthMeasureSpec)
@@ -5,6 +5,10 @@ export interface CloxPickerTab {
5
5
  /** Optional icon (local file or remote URL) */
6
6
  icon?: {
7
7
  uri: string;
8
+ /** Icon width in points/dp (default: 20) */
9
+ width?: number;
10
+ /** Icon height in points/dp (default: 20) */
11
+ height?: number;
8
12
  };
9
13
  /** Display label */
10
14
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"CloxPicker.types.d.ts","sourceRoot":"","sources":["../src/CloxPicker.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAExC,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,IAAI,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACvB,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,mBAAmB;IAClC,0CAA0C;IAC1C,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACpD;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B"}
1
+ {"version":3,"file":"CloxPicker.types.d.ts","sourceRoot":"","sources":["../src/CloxPicker.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAExC,+CAA+C;AAC/C,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,IAAI,CAAC,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;QACZ,4CAA4C;QAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,6CAA6C;QAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,mBAAmB;IAClC,0CAA0C;IAC1C,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACpD;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"CloxPicker.types.js","sourceRoot":"","sources":["../src/CloxPicker.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\nexport type CloxPickerModuleEvents = {};\n\n/** Single tab item for the segmented picker */\nexport interface CloxPickerTab {\n /** Optional icon (local file or remote URL) */\n icon?: { uri: string };\n /** Display label */\n name: string;\n /** Stable id (used as key; can match index) */\n id: number;\n}\n\nexport interface CloxPickerViewProps {\n /** Tab items (icon optional, name, id) */\n tabs: CloxPickerTab[];\n /** Height of the picker in points/dp */\n height: number;\n /** Current selected tab index */\n value: number;\n /** Called when user selects a different tab (passes tab index) */\n onTabChange?: (event: { tabIndex: number }) => void;\n /**\n * iOS only: use Liquid Glass effect when available (iOS 26+).\n * When false, uses the same non–liquid-glass look on all iOS versions.\n * Default: true.\n */\n useLiquidGlass?: boolean;\n /** Color for selected tab (hex string, e.g., \"#007AFF\" or \"blue\") */\n selectedColor?: string;\n style?: StyleProp<ViewStyle>;\n}\n"]}
1
+ {"version":3,"file":"CloxPicker.types.js","sourceRoot":"","sources":["../src/CloxPicker.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { StyleProp, ViewStyle } from 'react-native';\n\nexport type CloxPickerModuleEvents = {};\n\n/** Single tab item for the segmented picker */\nexport interface CloxPickerTab {\n /** Optional icon (local file or remote URL) */\n icon?: { \n uri: string;\n /** Icon width in points/dp (default: 20) */\n width?: number;\n /** Icon height in points/dp (default: 20) */\n height?: number;\n };\n /** Display label */\n name: string;\n /** Stable id (used as key; can match index) */\n id: number;\n}\n\nexport interface CloxPickerViewProps {\n /** Tab items (icon optional, name, id) */\n tabs: CloxPickerTab[];\n /** Height of the picker in points/dp */\n height: number;\n /** Current selected tab index */\n value: number;\n /** Called when user selects a different tab (passes tab index) */\n onTabChange?: (event: { tabIndex: number }) => void;\n /**\n * iOS only: use Liquid Glass effect when available (iOS 26+).\n * When false, uses the same non–liquid-glass look on all iOS versions.\n * Default: true.\n */\n useLiquidGlass?: boolean;\n /** Color for selected tab (hex string, e.g., \"#007AFF\" or \"blue\") */\n selectedColor?: string;\n style?: StyleProp<ViewStyle>;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"CloxPickerView.d.ts","sourceRoot":"","sources":["../src/CloxPickerView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAI9D,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,qBAExD;AAED,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"CloxPickerView.d.ts","sourceRoot":"","sources":["../src/CloxPickerView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAI9D,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,qBAiBxD;AAED,eAAe,cAAc,CAAC"}
@@ -2,7 +2,19 @@ import { requireNativeView } from 'expo';
2
2
  import * as React from 'react';
3
3
  const NativeCloxPickerView = requireNativeView('CloxPicker');
4
4
  export function CloxPickerView(props) {
5
- return <NativeCloxPickerView {...props}/>;
5
+ const { onTabChange, ...restProps } = props;
6
+ const handleTabChange = React.useCallback((event) => {
7
+ if (!onTabChange)
8
+ return;
9
+ // Extract tabIndex from nativeEvent (React Native wraps native events)
10
+ // The event structure is: { nativeEvent: { tabIndex: number } }
11
+ const tabIndex = event?.nativeEvent?.tabIndex ?? event?.tabIndex;
12
+ if (typeof tabIndex === 'number') {
13
+ // Call the user's callback with the cleaned format
14
+ onTabChange({ tabIndex });
15
+ }
16
+ }, [onTabChange]);
17
+ return <NativeCloxPickerView {...restProps} onTabChange={handleTabChange}/>;
6
18
  }
7
19
  export default CloxPickerView;
8
20
  //# sourceMappingURL=CloxPickerView.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CloxPickerView.js","sourceRoot":"","sources":["../src/CloxPickerView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,oBAAoB,GAAG,iBAAiB,CAAsB,YAAY,CAAC,CAAC;AAElF,MAAM,UAAU,cAAc,CAAC,KAA0B;IACvD,OAAO,CAAC,oBAAoB,CAAC,IAAI,KAAK,CAAC,EAAG,CAAC;AAC7C,CAAC;AAED,eAAe,cAAc,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\n\nimport type { CloxPickerViewProps } from './CloxPicker.types';\n\nconst NativeCloxPickerView = requireNativeView<CloxPickerViewProps>('CloxPicker');\n\nexport function CloxPickerView(props: CloxPickerViewProps) {\n return <NativeCloxPickerView {...props} />;\n}\n\nexport default CloxPickerView;\n"]}
1
+ {"version":3,"file":"CloxPickerView.js","sourceRoot":"","sources":["../src/CloxPickerView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,oBAAoB,GAAG,iBAAiB,CAAsB,YAAY,CAAC,CAAC;AAElF,MAAM,UAAU,cAAc,CAAC,KAA0B;IACvD,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,EAAE,GAAG,KAAK,CAAC;IAE5C,MAAM,eAAe,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC,KAAU,EAAE,EAAE;QACvD,IAAI,CAAC,WAAW;YAAE,OAAO;QAEzB,uEAAuE;QACvE,gEAAgE;QAChE,MAAM,QAAQ,GAAG,KAAK,EAAE,WAAW,EAAE,QAAQ,IAAI,KAAK,EAAE,QAAQ,CAAC;QAEjE,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,mDAAmD;YACnD,WAAW,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAElB,OAAO,CAAC,oBAAoB,CAAC,IAAI,SAAS,CAAC,CAAC,WAAW,CAAC,CAAC,eAAe,CAAC,EAAG,CAAC;AAC/E,CAAC;AAED,eAAe,cAAc,CAAC","sourcesContent":["import { requireNativeView } from 'expo';\nimport * as React from 'react';\n\nimport type { CloxPickerViewProps } from './CloxPicker.types';\n\nconst NativeCloxPickerView = requireNativeView<CloxPickerViewProps>('CloxPicker');\n\nexport function CloxPickerView(props: CloxPickerViewProps) {\n const { onTabChange, ...restProps } = props;\n \n const handleTabChange = React.useCallback((event: any) => {\n if (!onTabChange) return;\n \n // Extract tabIndex from nativeEvent (React Native wraps native events)\n // The event structure is: { nativeEvent: { tabIndex: number } }\n const tabIndex = event?.nativeEvent?.tabIndex ?? event?.tabIndex;\n \n if (typeof tabIndex === 'number') {\n // Call the user's callback with the cleaned format\n onTabChange({ tabIndex });\n }\n }, [onTabChange]);\n \n return <NativeCloxPickerView {...restProps} onTabChange={handleTabChange} />;\n}\n\nexport default CloxPickerView;\n"]}
package/example/App.tsx CHANGED
@@ -18,10 +18,10 @@ import {
18
18
  } from 'react-native';
19
19
 
20
20
  const DEFAULT_TABS: CloxPickerTab[] = [
21
- { id: 0, name: 'All', icon: { uri: 'https://cronoz-assets.s3.us-west-2.amazonaws.com/icons/play.png' } },
22
- { id: 1, name: 'Calendar', icon: { uri: 'https://cronoz-assets.s3.us-west-2.amazonaws.com/icons/upload.png' } },
23
- { id: 2, name: 'Planning', icon: { uri: 'https://cronoz-assets.s3.us-west-2.amazonaws.com/icons/male.png' } },
24
- { id: 3, name: 'Explore', icon: { uri: 'https://cronoz-assets.s3.us-west-2.amazonaws.com/icons/female.png' } },
21
+ { id: 0, name: 'All', icon: { uri: 'https://cronoz-assets.s3.us-west-2.amazonaws.com/icons/play.png', width: 20, height: 20 } },
22
+ { id: 1, name: 'Calendar', icon: { uri: 'https://cronoz-assets.s3.us-west-2.amazonaws.com/icons/upload.png', width: 20, height: 20 } },
23
+ { id: 2, name: 'Planning', icon: { uri: 'https://cronoz-assets.s3.us-west-2.amazonaws.com/icons/male.png', width: 15, height: 20 } },
24
+ { id: 3, name: 'Explore', icon: { uri: 'https://cronoz-assets.s3.us-west-2.amazonaws.com/icons/female.png', width: 20, height: 20 } },
25
25
  ];
26
26
 
27
27
  const PICKER_HEIGHT = 90;
@@ -29,7 +29,7 @@ const PICKER_HEIGHT = 90;
29
29
  export default function App() {
30
30
  const {width, height} = useWindowDimensions();
31
31
  const colorScheme = useColorScheme();
32
- const [liquidIndex, setLiquidIndex] = useState(0);
32
+ const [liquidIndex, setLiquidIndex] = useState(2);
33
33
  const [standardIndex, setStandardIndex] = useState(0);
34
34
  const [androidIndex, setAndroidIndex] = useState(0);
35
35
  const [currentScheme, setCurrentScheme] = useState<'light' | 'dark' | null>(null);
@@ -84,7 +84,9 @@ export default function App() {
84
84
  value={liquidIndex}
85
85
  useLiquidGlass={true}
86
86
  selectedColor="#FF6B6B"
87
- onTabChange={({ tabIndex }) => setLiquidIndex(tabIndex)}
87
+ onTabChange={({ tabIndex }) => {
88
+ setLiquidIndex(tabIndex);
89
+ }}
88
90
  style={styles.picker}
89
91
  />
90
92
  </View>
@@ -97,7 +99,9 @@ export default function App() {
97
99
  value={standardIndex}
98
100
  useLiquidGlass={false}
99
101
  selectedColor="#4ECDC4"
100
- onTabChange={({ tabIndex }) => setStandardIndex(tabIndex)}
102
+ onTabChange={({ tabIndex }) => {
103
+ setStandardIndex(tabIndex);
104
+ }}
101
105
  style={styles.picker}
102
106
  />
103
107
  </View>
@@ -106,13 +110,16 @@ export default function App() {
106
110
 
107
111
  {Platform.OS === 'android' && (
108
112
  <>
109
- <Text style={styles.title}>Segmented Picker</Text>
113
+ <Text style={styles.title}>Segmented Picker (Android)</Text>
110
114
  <View style={styles.pickerWrap}>
111
115
  <CloxPickerView
112
116
  tabs={DEFAULT_TABS}
113
117
  height={PICKER_HEIGHT}
114
118
  value={androidIndex}
115
- onTabChange={({ tabIndex }) => setAndroidIndex(tabIndex)}
119
+ selectedColor="#9B59B6"
120
+ onTabChange={({ tabIndex }) => {
121
+ setAndroidIndex(tabIndex);
122
+ }}
116
123
  style={styles.picker}
117
124
  />
118
125
  </View>
@@ -1,5 +1,5 @@
1
1
  PODS:
2
- - CloxPicker (0.1.0):
2
+ - CloxPicker (0.1.2):
3
3
  - ExpoModulesCore
4
4
  - EXConstants (18.0.13):
5
5
  - ExpoModulesCore
@@ -2123,7 +2123,7 @@ EXTERNAL SOURCES:
2123
2123
  :path: "../node_modules/react-native/ReactCommon/yoga"
2124
2124
 
2125
2125
  SPEC CHECKSUMS:
2126
- CloxPicker: ff3ee606156dc339341ef71eecbb35fa0a3eea73
2126
+ CloxPicker: 2230a7a323a09d77ac86c4a61fab545554f1838f
2127
2127
  EXConstants: 3feb66fd1d94202fc1f0946d74e029d8b224b60e
2128
2128
  EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd
2129
2129
  EXManifests: 83ef0844fcf06d6099b12a7bdbd7d36fc0e1dd16
@@ -7,12 +7,12 @@
7
7
  objects = {
8
8
 
9
9
  /* Begin PBXBuildFile section */
10
- 10E2FBE821ED63804F093245 /* libPods-cloxpickerexample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F2F4AF4CDFC07432659197DB /* libPods-cloxpickerexample.a */; };
11
10
  13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
12
11
  3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
13
- 6FE7D70859D7CF3D7E0FE935 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3E0A7701F08CCAAD7CC3316 /* ExpoModulesProvider.swift */; };
14
- B5580A65D519E4D823FDB38E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = DDD4E5B6438E6C7F4E265A19 /* PrivacyInfo.xcprivacy */; };
12
+ 6D60B6DE6807345550D89DB0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 515D0E48D4C6FB6251E34D77 /* PrivacyInfo.xcprivacy */; };
13
+ 9E00A3C2E448C751E924C5B6 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD77C7F2F5414CA8425C4F9 /* ExpoModulesProvider.swift */; };
15
14
  BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
15
+ E7390BD80697FEB360F986A7 /* libPods-cloxpickerexample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DACA0C66768CDB757B10EDDD /* libPods-cloxpickerexample.a */; };
16
16
  F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
17
17
  /* End PBXBuildFile section */
18
18
 
@@ -20,16 +20,16 @@
20
20
  13B07F961A680F5B00A75B9A /* cloxpickerexample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = cloxpickerexample.app; sourceTree = BUILT_PRODUCTS_DIR; };
21
21
  13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = cloxpickerexample/Images.xcassets; sourceTree = "<group>"; };
22
22
  13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = cloxpickerexample/Info.plist; sourceTree = "<group>"; };
23
- 3AB073BD8706BB4BEB67E225 /* Pods-cloxpickerexample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-cloxpickerexample.debug.xcconfig"; path = "Target Support Files/Pods-cloxpickerexample/Pods-cloxpickerexample.debug.xcconfig"; sourceTree = "<group>"; };
24
- 5363FF98F65652078EA56E87 /* Pods-cloxpickerexample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-cloxpickerexample.release.xcconfig"; path = "Target Support Files/Pods-cloxpickerexample/Pods-cloxpickerexample.release.xcconfig"; sourceTree = "<group>"; };
25
- A3E0A7701F08CCAAD7CC3316 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-cloxpickerexample/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
23
+ 3E28B125BC2D19E6432E207C /* Pods-cloxpickerexample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-cloxpickerexample.release.xcconfig"; path = "Target Support Files/Pods-cloxpickerexample/Pods-cloxpickerexample.release.xcconfig"; sourceTree = "<group>"; };
24
+ 515D0E48D4C6FB6251E34D77 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = cloxpickerexample/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
26
25
  AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = cloxpickerexample/SplashScreen.storyboard; sourceTree = "<group>"; };
27
26
  BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
28
- DDD4E5B6438E6C7F4E265A19 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = cloxpickerexample/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
27
+ DA3650119D47B51A62404F45 /* Pods-cloxpickerexample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-cloxpickerexample.debug.xcconfig"; path = "Target Support Files/Pods-cloxpickerexample/Pods-cloxpickerexample.debug.xcconfig"; sourceTree = "<group>"; };
28
+ DACA0C66768CDB757B10EDDD /* libPods-cloxpickerexample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-cloxpickerexample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
29
+ ECD77C7F2F5414CA8425C4F9 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-cloxpickerexample/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
29
30
  ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
30
31
  F11748412D0307B40044C1D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = cloxpickerexample/AppDelegate.swift; sourceTree = "<group>"; };
31
32
  F11748442D0722820044C1D9 /* cloxpickerexample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "cloxpickerexample-Bridging-Header.h"; path = "cloxpickerexample/cloxpickerexample-Bridging-Header.h"; sourceTree = "<group>"; };
32
- F2F4AF4CDFC07432659197DB /* libPods-cloxpickerexample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-cloxpickerexample.a"; sourceTree = BUILT_PRODUCTS_DIR; };
33
33
  /* End PBXFileReference section */
34
34
 
35
35
  /* Begin PBXFrameworksBuildPhase section */
@@ -37,21 +37,13 @@
37
37
  isa = PBXFrameworksBuildPhase;
38
38
  buildActionMask = 2147483647;
39
39
  files = (
40
- 10E2FBE821ED63804F093245 /* libPods-cloxpickerexample.a in Frameworks */,
40
+ E7390BD80697FEB360F986A7 /* libPods-cloxpickerexample.a in Frameworks */,
41
41
  );
42
42
  runOnlyForDeploymentPostprocessing = 0;
43
43
  };
44
44
  /* End PBXFrameworksBuildPhase section */
45
45
 
46
46
  /* Begin PBXGroup section */
47
- 0DD178F290DBF00252BD8B0E /* ExpoModulesProviders */ = {
48
- isa = PBXGroup;
49
- children = (
50
- 839C7BEB5F056EB94BBBB9C8 /* cloxpickerexample */,
51
- );
52
- name = ExpoModulesProviders;
53
- sourceTree = "<group>";
54
- };
55
47
  13B07FAE1A68108700A75B9A /* cloxpickerexample */ = {
56
48
  isa = PBXGroup;
57
49
  children = (
@@ -61,7 +53,7 @@
61
53
  13B07FB51A68108700A75B9A /* Images.xcassets */,
62
54
  13B07FB61A68108700A75B9A /* Info.plist */,
63
55
  AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
64
- DDD4E5B6438E6C7F4E265A19 /* PrivacyInfo.xcprivacy */,
56
+ 515D0E48D4C6FB6251E34D77 /* PrivacyInfo.xcprivacy */,
65
57
  );
66
58
  name = cloxpickerexample;
67
59
  sourceTree = "<group>";
@@ -70,16 +62,16 @@
70
62
  isa = PBXGroup;
71
63
  children = (
72
64
  ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
73
- F2F4AF4CDFC07432659197DB /* libPods-cloxpickerexample.a */,
65
+ DACA0C66768CDB757B10EDDD /* libPods-cloxpickerexample.a */,
74
66
  );
75
67
  name = Frameworks;
76
68
  sourceTree = "<group>";
77
69
  };
78
- 62F14DA2B93074519377E87C /* Pods */ = {
70
+ 4BB04C227EE42226DB6018D2 /* Pods */ = {
79
71
  isa = PBXGroup;
80
72
  children = (
81
- 3AB073BD8706BB4BEB67E225 /* Pods-cloxpickerexample.debug.xcconfig */,
82
- 5363FF98F65652078EA56E87 /* Pods-cloxpickerexample.release.xcconfig */,
73
+ DA3650119D47B51A62404F45 /* Pods-cloxpickerexample.debug.xcconfig */,
74
+ 3E28B125BC2D19E6432E207C /* Pods-cloxpickerexample.release.xcconfig */,
83
75
  );
84
76
  name = Pods;
85
77
  path = Pods;
@@ -92,14 +84,6 @@
92
84
  name = Libraries;
93
85
  sourceTree = "<group>";
94
86
  };
95
- 839C7BEB5F056EB94BBBB9C8 /* cloxpickerexample */ = {
96
- isa = PBXGroup;
97
- children = (
98
- A3E0A7701F08CCAAD7CC3316 /* ExpoModulesProvider.swift */,
99
- );
100
- name = cloxpickerexample;
101
- sourceTree = "<group>";
102
- };
103
87
  83CBB9F61A601CBA00E9B192 = {
104
88
  isa = PBXGroup;
105
89
  children = (
@@ -107,8 +91,8 @@
107
91
  832341AE1AAA6A7D00B99B32 /* Libraries */,
108
92
  83CBBA001A601CBA00E9B192 /* Products */,
109
93
  2D16E6871FA4F8E400B85C8A /* Frameworks */,
110
- 62F14DA2B93074519377E87C /* Pods */,
111
- 0DD178F290DBF00252BD8B0E /* ExpoModulesProviders */,
94
+ 4BB04C227EE42226DB6018D2 /* Pods */,
95
+ D6AE54B70D5F7EF429EC83E1 /* ExpoModulesProviders */,
112
96
  );
113
97
  indentWidth = 2;
114
98
  sourceTree = "<group>";
@@ -123,6 +107,14 @@
123
107
  name = Products;
124
108
  sourceTree = "<group>";
125
109
  };
110
+ 95E4A94D9637BA1B96DDF31A /* cloxpickerexample */ = {
111
+ isa = PBXGroup;
112
+ children = (
113
+ ECD77C7F2F5414CA8425C4F9 /* ExpoModulesProvider.swift */,
114
+ );
115
+ name = cloxpickerexample;
116
+ sourceTree = "<group>";
117
+ };
126
118
  BB2F792B24A3F905000567C9 /* Supporting */ = {
127
119
  isa = PBXGroup;
128
120
  children = (
@@ -132,6 +124,14 @@
132
124
  path = cloxpickerexample/Supporting;
133
125
  sourceTree = "<group>";
134
126
  };
127
+ D6AE54B70D5F7EF429EC83E1 /* ExpoModulesProviders */ = {
128
+ isa = PBXGroup;
129
+ children = (
130
+ 95E4A94D9637BA1B96DDF31A /* cloxpickerexample */,
131
+ );
132
+ name = ExpoModulesProviders;
133
+ sourceTree = "<group>";
134
+ };
135
135
  /* End PBXGroup section */
136
136
 
137
137
  /* Begin PBXNativeTarget section */
@@ -140,13 +140,13 @@
140
140
  buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "cloxpickerexample" */;
141
141
  buildPhases = (
142
142
  08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
143
- 26179FA961D47EAF929A4703 /* [Expo] Configure project */,
143
+ E9EEC3F2A013D05DDDABD8F7 /* [Expo] Configure project */,
144
144
  13B07F871A680F5B00A75B9A /* Sources */,
145
145
  13B07F8C1A680F5B00A75B9A /* Frameworks */,
146
146
  13B07F8E1A680F5B00A75B9A /* Resources */,
147
147
  00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
148
148
  800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
149
- C8E1FEBD83DC50D9800C4D33 /* [CP] Embed Pods Frameworks */,
149
+ D0BCA59BDC991A129B60063A /* [CP] Embed Pods Frameworks */,
150
150
  );
151
151
  buildRules = (
152
152
  );
@@ -198,7 +198,7 @@
198
198
  BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
199
199
  13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
200
200
  3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
201
- B5580A65D519E4D823FDB38E /* PrivacyInfo.xcprivacy in Resources */,
201
+ 6D60B6DE6807345550D89DB0 /* PrivacyInfo.xcprivacy in Resources */,
202
202
  );
203
203
  runOnlyForDeploymentPostprocessing = 0;
204
204
  };
@@ -244,30 +244,6 @@
244
244
  shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
245
245
  showEnvVarsInLog = 0;
246
246
  };
247
- 26179FA961D47EAF929A4703 /* [Expo] Configure project */ = {
248
- isa = PBXShellScriptBuildPhase;
249
- alwaysOutOfDate = 1;
250
- buildActionMask = 2147483647;
251
- files = (
252
- );
253
- inputFileListPaths = (
254
- );
255
- inputPaths = (
256
- "$(SRCROOT)/.xcode.env",
257
- "$(SRCROOT)/.xcode.env.local",
258
- "$(SRCROOT)/cloxpickerexample/cloxpickerexample.entitlements",
259
- "$(SRCROOT)/Pods/Target Support Files/Pods-cloxpickerexample/expo-configure-project.sh",
260
- );
261
- name = "[Expo] Configure project";
262
- outputFileListPaths = (
263
- );
264
- outputPaths = (
265
- "$(SRCROOT)/Pods/Target Support Files/Pods-cloxpickerexample/ExpoModulesProvider.swift",
266
- );
267
- runOnlyForDeploymentPostprocessing = 0;
268
- shellPath = /bin/sh;
269
- shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-cloxpickerexample/expo-configure-project.sh\"\n";
270
- };
271
247
  800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
272
248
  isa = PBXShellScriptBuildPhase;
273
249
  buildActionMask = 2147483647;
@@ -298,7 +274,7 @@
298
274
  shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-cloxpickerexample/Pods-cloxpickerexample-resources.sh\"\n";
299
275
  showEnvVarsInLog = 0;
300
276
  };
301
- C8E1FEBD83DC50D9800C4D33 /* [CP] Embed Pods Frameworks */ = {
277
+ D0BCA59BDC991A129B60063A /* [CP] Embed Pods Frameworks */ = {
302
278
  isa = PBXShellScriptBuildPhase;
303
279
  buildActionMask = 2147483647;
304
280
  files = (
@@ -320,6 +296,30 @@
320
296
  shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-cloxpickerexample/Pods-cloxpickerexample-frameworks.sh\"\n";
321
297
  showEnvVarsInLog = 0;
322
298
  };
299
+ E9EEC3F2A013D05DDDABD8F7 /* [Expo] Configure project */ = {
300
+ isa = PBXShellScriptBuildPhase;
301
+ alwaysOutOfDate = 1;
302
+ buildActionMask = 2147483647;
303
+ files = (
304
+ );
305
+ inputFileListPaths = (
306
+ );
307
+ inputPaths = (
308
+ "$(SRCROOT)/.xcode.env",
309
+ "$(SRCROOT)/.xcode.env.local",
310
+ "$(SRCROOT)/cloxpickerexample/cloxpickerexample.entitlements",
311
+ "$(SRCROOT)/Pods/Target Support Files/Pods-cloxpickerexample/expo-configure-project.sh",
312
+ );
313
+ name = "[Expo] Configure project";
314
+ outputFileListPaths = (
315
+ );
316
+ outputPaths = (
317
+ "$(SRCROOT)/Pods/Target Support Files/Pods-cloxpickerexample/ExpoModulesProvider.swift",
318
+ );
319
+ runOnlyForDeploymentPostprocessing = 0;
320
+ shellPath = /bin/sh;
321
+ shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-cloxpickerexample/expo-configure-project.sh\"\n";
322
+ };
323
323
  /* End PBXShellScriptBuildPhase section */
324
324
 
325
325
  /* Begin PBXSourcesBuildPhase section */
@@ -328,7 +328,7 @@
328
328
  buildActionMask = 2147483647;
329
329
  files = (
330
330
  F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */,
331
- 6FE7D70859D7CF3D7E0FE935 /* ExpoModulesProvider.swift in Sources */,
331
+ 9E00A3C2E448C751E924C5B6 /* ExpoModulesProvider.swift in Sources */,
332
332
  );
333
333
  runOnlyForDeploymentPostprocessing = 0;
334
334
  };
@@ -337,7 +337,7 @@
337
337
  /* Begin XCBuildConfiguration section */
338
338
  13B07F941A680F5B00A75B9A /* Debug */ = {
339
339
  isa = XCBuildConfiguration;
340
- baseConfigurationReference = 3AB073BD8706BB4BEB67E225 /* Pods-cloxpickerexample.debug.xcconfig */;
340
+ baseConfigurationReference = DA3650119D47B51A62404F45 /* Pods-cloxpickerexample.debug.xcconfig */;
341
341
  buildSettings = {
342
342
  ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
343
343
  CLANG_ENABLE_MODULES = YES;
@@ -376,7 +376,7 @@
376
376
  };
377
377
  13B07F951A680F5B00A75B9A /* Release */ = {
378
378
  isa = XCBuildConfiguration;
379
- baseConfigurationReference = 5363FF98F65652078EA56E87 /* Pods-cloxpickerexample.release.xcconfig */;
379
+ baseConfigurationReference = 3E28B125BC2D19E6432E207C /* Pods-cloxpickerexample.release.xcconfig */;
380
380
  buildSettings = {
381
381
  ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
382
382
  CLANG_ENABLE_MODULES = YES;
@@ -7,10 +7,14 @@ struct CloxPickerTabItem: Identifiable {
7
7
  let id: Int
8
8
  let name: String
9
9
  let iconUri: String?
10
+ let iconWidth: CGFloat?
11
+ let iconHeight: CGFloat?
10
12
  }
11
13
 
12
14
  // MARK: - UITabBar wrapper for horizontal segmented control
13
15
  class HorizontalTabBar: UITabBar {
16
+ var isUpdatingProgrammatically = false
17
+
14
18
  override func layoutSubviews() {
15
19
  super.layoutSubviews()
16
20
 
@@ -27,6 +31,17 @@ class HorizontalTabBar: UITabBar {
27
31
  height: bounds.height
28
32
  )
29
33
  xPosition += itemWidth
34
+ // Ensure touch events are properly handled
35
+ itemView.isExclusiveTouch = true
36
+ }
37
+ }
38
+ }
39
+
40
+ override var selectedItem: UITabBarItem? {
41
+ didSet {
42
+ // Only prevent delegate if we're updating programmatically
43
+ if isUpdatingProgrammatically {
44
+ return
30
45
  }
31
46
  }
32
47
  }
@@ -38,10 +53,15 @@ struct UIKitTabBarControl: UIViewRepresentable {
38
53
  @Binding var selectedIndex: Int
39
54
  let height: CGFloat
40
55
  let selectedColor: UIColor?
56
+ let onSelectionChange: ((Int) -> Void)?
57
+ let expoView: CloxPickerView?
41
58
 
42
59
  func makeUIView(context: Context) -> HorizontalTabBar {
43
60
  let tabBar = HorizontalTabBar()
44
61
  tabBar.isTranslucent = true
62
+ // Ensure touch events are properly handled
63
+ tabBar.isUserInteractionEnabled = true
64
+ tabBar.isMultipleTouchEnabled = false
45
65
 
46
66
  // Create tab bar items with icons
47
67
  let tabBarItems = tabs.enumerated().map { index, tab in
@@ -49,12 +69,17 @@ struct UIKitTabBarControl: UIViewRepresentable {
49
69
 
50
70
  // Load icon if provided
51
71
  if let iconUri = tab.iconUri {
72
+ let iconWidth = tab.iconWidth ?? 20
73
+ let iconHeight = tab.iconHeight ?? 20
74
+
52
75
  if let url = URL(string: iconUri) {
53
76
  // Load image asynchronously
54
77
  URLSession.shared.dataTask(with: url) { data, _, _ in
55
78
  if let data = data, let image = UIImage(data: data) {
79
+ // Resize image to per-icon dimensions
80
+ let resizedImage = resizeImage(image, to: CGSize(width: iconWidth, height: iconHeight))
56
81
  // Use template mode so icon adapts to light/dark mode (black/white)
57
- let renderedImage = image.withRenderingMode(.alwaysTemplate)
82
+ let renderedImage = resizedImage.withRenderingMode(.alwaysTemplate)
58
83
  DispatchQueue.main.async {
59
84
  item.image = renderedImage
60
85
  }
@@ -63,7 +88,8 @@ struct UIKitTabBarControl: UIViewRepresentable {
63
88
  } else if iconUri.hasPrefix("/") {
64
89
  // Local file
65
90
  if let image = UIImage(contentsOfFile: iconUri) {
66
- item.image = image.withRenderingMode(.alwaysTemplate)
91
+ let resizedImage = resizeImage(image, to: CGSize(width: iconWidth, height: iconHeight))
92
+ item.image = resizedImage.withRenderingMode(.alwaysTemplate)
67
93
  }
68
94
  }
69
95
  }
@@ -72,7 +98,11 @@ struct UIKitTabBarControl: UIViewRepresentable {
72
98
  }
73
99
  tabBar.setItems(tabBarItems, animated: false)
74
100
  if selectedIndex < tabBarItems.count {
101
+ tabBar.isUpdatingProgrammatically = true
75
102
  tabBar.selectedItem = tabBarItems[selectedIndex]
103
+ DispatchQueue.main.async {
104
+ tabBar.isUpdatingProgrammatically = false
105
+ }
76
106
  }
77
107
 
78
108
  // Configure appearance for iOS 26 liquid glass
@@ -112,6 +142,14 @@ struct UIKitTabBarControl: UIViewRepresentable {
112
142
  }
113
143
 
114
144
  func updateUIView(_ uiView: HorizontalTabBar, context: Context) {
145
+ // Update coordinator's parent reference to get latest callback
146
+ // This ensures the coordinator always has the latest onSelectionChange callback
147
+ context.coordinator.parent = self
148
+ // Ensure delegate is set (in case it was cleared)
149
+ if uiView.delegate !== context.coordinator {
150
+ uiView.delegate = context.coordinator
151
+ }
152
+
115
153
  // Update selected color if changed
116
154
  if let selectedColor = selectedColor {
117
155
  if #available(iOS 26.0, *) {
@@ -140,11 +178,16 @@ struct UIKitTabBarControl: UIViewRepresentable {
140
178
  let tabBarItems = tabs.enumerated().map { index, tab in
141
179
  let item = UITabBarItem(title: tab.name, image: nil, tag: index)
142
180
  if let iconUri = tab.iconUri {
181
+ let iconWidth = tab.iconWidth ?? 20
182
+ let iconHeight = tab.iconHeight ?? 20
183
+
143
184
  if let url = URL(string: iconUri) {
144
185
  URLSession.shared.dataTask(with: url) { data, _, _ in
145
186
  if let data = data, let image = UIImage(data: data) {
187
+ // Resize image to per-icon dimensions
188
+ let resizedImage = resizeImage(image, to: CGSize(width: iconWidth, height: iconHeight))
146
189
  // Use template mode so icon adapts to light/dark mode (black/white)
147
- let renderedImage = image.withRenderingMode(.alwaysTemplate)
190
+ let renderedImage = resizedImage.withRenderingMode(.alwaysTemplate)
148
191
  DispatchQueue.main.async {
149
192
  item.image = renderedImage
150
193
  }
@@ -152,7 +195,8 @@ struct UIKitTabBarControl: UIViewRepresentable {
152
195
  }.resume()
153
196
  } else if iconUri.hasPrefix("/") {
154
197
  if let image = UIImage(contentsOfFile: iconUri) {
155
- item.image = image.withRenderingMode(.alwaysTemplate)
198
+ let resizedImage = resizeImage(image, to: CGSize(width: iconWidth, height: iconHeight))
199
+ item.image = resizedImage.withRenderingMode(.alwaysTemplate)
156
200
  }
157
201
  }
158
202
  }
@@ -161,9 +205,18 @@ struct UIKitTabBarControl: UIViewRepresentable {
161
205
  uiView.setItems(tabBarItems, animated: false)
162
206
  }
163
207
 
164
- // Update selection
208
+ // Always update selection when selectedIndex changes (programmatically)
165
209
  if let items = uiView.items, selectedIndex < items.count {
166
- uiView.selectedItem = items[selectedIndex]
210
+ let currentSelectedTag = uiView.selectedItem?.tag ?? -1
211
+ if currentSelectedTag != selectedIndex {
212
+ // Mark as programmatic update to prevent delegate callback
213
+ uiView.isUpdatingProgrammatically = true
214
+ uiView.selectedItem = items[selectedIndex]
215
+ // Reset flag after a brief delay to allow UI to update
216
+ DispatchQueue.main.async {
217
+ uiView.isUpdatingProgrammatically = false
218
+ }
219
+ }
167
220
  }
168
221
  }
169
222
 
@@ -179,9 +232,36 @@ struct UIKitTabBarControl: UIViewRepresentable {
179
232
  }
180
233
 
181
234
  func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
182
- parent.selectedIndex = item.tag
235
+ // Only handle user-initiated selections, not programmatic ones
236
+ if let horizontalTabBar = tabBar as? HorizontalTabBar, horizontalTabBar.isUpdatingProgrammatically {
237
+ return
238
+ }
239
+
240
+ let newIndex = item.tag
241
+
242
+ // Call the callback FIRST before updating binding to avoid SwiftUI update conflicts
243
+ parent.onSelectionChange?(newIndex)
244
+
245
+ // Also dispatch event directly from ExpoView if available
246
+ if let expoView = parent.expoView {
247
+ expoView.dispatchTabChange(index: newIndex)
248
+ }
249
+
250
+ // Then update binding (this will trigger SwiftUI update, but callback already fired)
251
+ DispatchQueue.main.async { [weak self] in
252
+ self?.parent.selectedIndex = newIndex
253
+ }
183
254
  }
184
255
  }
256
+
257
+ }
258
+
259
+ // Helper function to resize images
260
+ private func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage {
261
+ let renderer = UIGraphicsImageRenderer(size: size)
262
+ return renderer.image { _ in
263
+ image.draw(in: CGRect(origin: .zero, size: size))
264
+ }
185
265
  }
186
266
 
187
267
  // MARK: - Segmented Picker using TabView's tab bar
@@ -191,6 +271,7 @@ struct SegmentedPickerView: View {
191
271
  @Binding var selectedIndex: Int
192
272
  let useLiquidGlass: Bool
193
273
  let selectedColor: UIColor?
274
+ let expoView: CloxPickerView?
194
275
  let onSelectionChange: (Int) -> Void
195
276
 
196
277
  var body: some View {
@@ -198,14 +279,13 @@ struct SegmentedPickerView: View {
198
279
  tabs: tabs,
199
280
  selectedIndex: $selectedIndex,
200
281
  height: height,
201
- selectedColor: selectedColor
282
+ selectedColor: selectedColor,
283
+ onSelectionChange: onSelectionChange,
284
+ expoView: expoView
202
285
  )
203
286
  .frame(maxWidth: .infinity, minHeight: height)
204
287
  .frame(height: height)
205
288
  .clipped()
206
- .onChange(of: selectedIndex) { newValue in
207
- onSelectionChange(newValue)
208
- }
209
289
  }
210
290
  }
211
291
 
@@ -224,16 +304,33 @@ public class CloxPickerView: ExpoView {
224
304
  super.init(appContext: appContext)
225
305
  backgroundColor = .clear
226
306
  }
307
+
308
+ func dispatchTabChange(index: Int) {
309
+ onTabChange(["tabIndex": index])
310
+ }
227
311
 
228
312
  func setTabs(_ tabs: [[String: Any]]) {
229
313
  currentTabs = tabs.compactMap { dict -> CloxPickerTabItem? in
230
314
  guard let name = dict["name"] as? String,
231
315
  let id = dict["id"] as? Int else { return nil }
232
- let icon = (dict["icon"] as? [String: Any])?["uri"] as? String
233
- return CloxPickerTabItem(id: id, name: name, iconUri: icon)
316
+ let iconDict = dict["icon"] as? [String: Any]
317
+ let iconUri = iconDict?["uri"] as? String
318
+ let iconWidth = iconDict?["width"] as? Double
319
+ let iconHeight = iconDict?["height"] as? Double
320
+ return CloxPickerTabItem(
321
+ id: id,
322
+ name: name,
323
+ iconUri: iconUri,
324
+ iconWidth: iconWidth != nil ? CGFloat(iconWidth!) : nil,
325
+ iconHeight: iconHeight != nil ? CGFloat(iconHeight!) : nil
326
+ )
234
327
  }
235
328
  if currentTabs.isEmpty {
236
- currentTabs = [CloxPickerTabItem(id: 0, name: "Tab", iconUri: nil)]
329
+ currentTabs = [CloxPickerTabItem(id: 0, name: "Tab", iconUri: nil, iconWidth: nil, iconHeight: nil)]
330
+ }
331
+ // Re-validate currentValue against the new tabs count
332
+ if currentTabs.count > 0 {
333
+ currentValue = max(0, min(currentValue, currentTabs.count - 1))
237
334
  }
238
335
  updateHostingView()
239
336
  }
@@ -244,8 +341,22 @@ public class CloxPickerView: ExpoView {
244
341
  }
245
342
 
246
343
  func setValue(_ value: Int) {
247
- currentValue = max(0, min(value, currentTabs.count - 1))
248
- updateHostingView()
344
+ // If tabs haven't been set yet, store the value as-is (will be validated when tabs are set)
345
+ // Otherwise, clamp to valid range
346
+ let newValue: Int
347
+ if currentTabs.isEmpty {
348
+ newValue = value
349
+ } else {
350
+ newValue = max(0, min(value, currentTabs.count - 1))
351
+ }
352
+
353
+ if newValue != currentValue {
354
+ currentValue = newValue
355
+ // Force update the hosting view to reflect the new value
356
+ DispatchQueue.main.async { [weak self] in
357
+ self?.updateHostingView()
358
+ }
359
+ }
249
360
  }
250
361
 
251
362
  func setUseLiquidGlass(_ use: Bool) {
@@ -275,14 +386,21 @@ public class CloxPickerView: ExpoView {
275
386
  selectedIndex: binding,
276
387
  useLiquidGlass: useLiquidGlass,
277
388
  selectedColor: selectedColor,
389
+ expoView: self,
278
390
  onSelectionChange: { [weak self] index in
279
- self?.currentValue = index
280
- self?.onTabChange(["tabIndex": index])
391
+ guard let self = self else { return }
392
+ // Update current value
393
+ self.currentValue = index
394
+ // Dispatch event synchronously (we're already on main thread from delegate)
395
+ self.onTabChange(["tabIndex": index])
281
396
  }
282
397
  )
283
398
 
284
399
  if let existing = hostingController {
400
+ // Force SwiftUI to update by replacing the root view
285
401
  existing.rootView = rootView
402
+ // Ensure the view is marked as needing update
403
+ existing.view.setNeedsLayout()
286
404
  return
287
405
  }
288
406
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clox-picker",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Clox Picker native module",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -5,7 +5,13 @@ export type CloxPickerModuleEvents = {};
5
5
  /** Single tab item for the segmented picker */
6
6
  export interface CloxPickerTab {
7
7
  /** Optional icon (local file or remote URL) */
8
- icon?: { uri: string };
8
+ icon?: {
9
+ uri: string;
10
+ /** Icon width in points/dp (default: 20) */
11
+ width?: number;
12
+ /** Icon height in points/dp (default: 20) */
13
+ height?: number;
14
+ };
9
15
  /** Display label */
10
16
  name: string;
11
17
  /** Stable id (used as key; can match index) */
@@ -6,7 +6,22 @@ import type { CloxPickerViewProps } from './CloxPicker.types';
6
6
  const NativeCloxPickerView = requireNativeView<CloxPickerViewProps>('CloxPicker');
7
7
 
8
8
  export function CloxPickerView(props: CloxPickerViewProps) {
9
- return <NativeCloxPickerView {...props} />;
9
+ const { onTabChange, ...restProps } = props;
10
+
11
+ const handleTabChange = React.useCallback((event: any) => {
12
+ if (!onTabChange) return;
13
+
14
+ // Extract tabIndex from nativeEvent (React Native wraps native events)
15
+ // The event structure is: { nativeEvent: { tabIndex: number } }
16
+ const tabIndex = event?.nativeEvent?.tabIndex ?? event?.tabIndex;
17
+
18
+ if (typeof tabIndex === 'number') {
19
+ // Call the user's callback with the cleaned format
20
+ onTabChange({ tabIndex });
21
+ }
22
+ }, [onTabChange]);
23
+
24
+ return <NativeCloxPickerView {...restProps} onTabChange={handleTabChange} />;
10
25
  }
11
26
 
12
27
  export default CloxPickerView;