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.
- package/android/src/main/java/expo/modules/cloxpicker/CloxPickerModule.kt +3 -0
- package/android/src/main/java/expo/modules/cloxpicker/CloxPickerView.kt +215 -21
- package/build/CloxPicker.types.d.ts +4 -0
- package/build/CloxPicker.types.d.ts.map +1 -1
- package/build/CloxPicker.types.js.map +1 -1
- package/build/CloxPickerView.d.ts.map +1 -1
- package/build/CloxPickerView.js +13 -1
- package/build/CloxPickerView.js.map +1 -1
- package/example/App.tsx +16 -9
- package/example/ios/Podfile.lock +2 -2
- package/example/ios/cloxpickerexample.xcodeproj/project.pbxproj +63 -63
- package/ios/CloxPickerView.swift +136 -18
- package/package.json +1 -1
- package/src/CloxPicker.types.ts +7 -1
- package/src/CloxPickerView.tsx +16 -1
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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;
|
|
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,
|
|
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"}
|
package/build/CloxPickerView.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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(
|
|
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 }) =>
|
|
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 }) =>
|
|
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
|
-
|
|
119
|
+
selectedColor="#9B59B6"
|
|
120
|
+
onTabChange={({ tabIndex }) => {
|
|
121
|
+
setAndroidIndex(tabIndex);
|
|
122
|
+
}}
|
|
116
123
|
style={styles.picker}
|
|
117
124
|
/>
|
|
118
125
|
</View>
|
package/example/ios/Podfile.lock
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
PODS:
|
|
2
|
-
- CloxPicker (0.1.
|
|
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:
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
DACA0C66768CDB757B10EDDD /* libPods-cloxpickerexample.a */,
|
|
74
66
|
);
|
|
75
67
|
name = Frameworks;
|
|
76
68
|
sourceTree = "<group>";
|
|
77
69
|
};
|
|
78
|
-
|
|
70
|
+
4BB04C227EE42226DB6018D2 /* Pods */ = {
|
|
79
71
|
isa = PBXGroup;
|
|
80
72
|
children = (
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
379
|
+
baseConfigurationReference = 3E28B125BC2D19E6432E207C /* Pods-cloxpickerexample.release.xcconfig */;
|
|
380
380
|
buildSettings = {
|
|
381
381
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
382
382
|
CLANG_ENABLE_MODULES = YES;
|
package/ios/CloxPickerView.swift
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
208
|
+
// Always update selection when selectedIndex changes (programmatically)
|
|
165
209
|
if let items = uiView.items, selectedIndex < items.count {
|
|
166
|
-
uiView.selectedItem
|
|
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
|
-
|
|
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
|
|
233
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
|
280
|
-
|
|
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
package/src/CloxPicker.types.ts
CHANGED
|
@@ -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?: {
|
|
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) */
|
package/src/CloxPickerView.tsx
CHANGED
|
@@ -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
|
-
|
|
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;
|