clox-picker 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.eslintrc.js +2 -0
  2. package/README.md +35 -0
  3. package/android/build.gradle +42 -0
  4. package/android/src/main/java/expo/modules/cloxpicker/CloxPickerModule.kt +30 -0
  5. package/android/src/main/java/expo/modules/cloxpicker/CloxPickerView.kt +173 -0
  6. package/build/CloxPicker.types.d.ts +35 -0
  7. package/build/CloxPicker.types.d.ts.map +1 -0
  8. package/build/CloxPicker.types.js +2 -0
  9. package/build/CloxPicker.types.js.map +1 -0
  10. package/build/CloxPickerModule.d.ts +9 -0
  11. package/build/CloxPickerModule.d.ts.map +1 -0
  12. package/build/CloxPickerModule.js +3 -0
  13. package/build/CloxPickerModule.js.map +1 -0
  14. package/build/CloxPickerView.d.ts +5 -0
  15. package/build/CloxPickerView.d.ts.map +1 -0
  16. package/build/CloxPickerView.js +8 -0
  17. package/build/CloxPickerView.js.map +1 -0
  18. package/build/index.d.ts +6 -0
  19. package/build/index.d.ts.map +1 -0
  20. package/build/index.js +11 -0
  21. package/build/index.js.map +1 -0
  22. package/example/App.tsx +158 -0
  23. package/example/android/app/build.gradle +182 -0
  24. package/example/android/app/debug.keystore +0 -0
  25. package/example/android/app/proguard-rules.pro +14 -0
  26. package/example/android/app/src/debug/AndroidManifest.xml +7 -0
  27. package/example/android/app/src/debugOptimized/AndroidManifest.xml +7 -0
  28. package/example/android/app/src/main/AndroidManifest.xml +31 -0
  29. package/example/android/app/src/main/java/expo/modules/cloxpicker/example/MainActivity.kt +61 -0
  30. package/example/android/app/src/main/java/expo/modules/cloxpicker/example/MainApplication.kt +56 -0
  31. package/example/android/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
  32. package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  33. package/example/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
  34. package/example/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
  35. package/example/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
  36. package/example/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
  37. package/example/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
  38. package/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  39. package/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  40. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  41. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
  42. package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  43. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  44. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
  45. package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  46. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  47. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
  48. package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  49. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  50. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
  51. package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  52. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  53. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
  54. package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  55. package/example/android/app/src/main/res/values/colors.xml +6 -0
  56. package/example/android/app/src/main/res/values/strings.xml +5 -0
  57. package/example/android/app/src/main/res/values/styles.xml +11 -0
  58. package/example/android/app/src/main/res/values-night/colors.xml +1 -0
  59. package/example/android/build.gradle +24 -0
  60. package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  61. package/example/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  62. package/example/android/gradle.properties +65 -0
  63. package/example/android/gradlew +251 -0
  64. package/example/android/gradlew.bat +94 -0
  65. package/example/android/settings.gradle +39 -0
  66. package/example/app.json +30 -0
  67. package/example/assets/adaptive-icon.png +0 -0
  68. package/example/assets/favicon.png +0 -0
  69. package/example/assets/icon.png +0 -0
  70. package/example/assets/splash-icon.png +0 -0
  71. package/example/index.ts +5 -0
  72. package/example/ios/.xcode.env +11 -0
  73. package/example/ios/Podfile +60 -0
  74. package/example/ios/Podfile.lock +2211 -0
  75. package/example/ios/Podfile.properties.json +5 -0
  76. package/example/ios/cloxpickerexample/AppDelegate.swift +70 -0
  77. package/example/ios/cloxpickerexample/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
  78. package/example/ios/cloxpickerexample/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
  79. package/example/ios/cloxpickerexample/Images.xcassets/Contents.json +6 -0
  80. package/example/ios/cloxpickerexample/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
  81. package/example/ios/cloxpickerexample/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +23 -0
  82. package/example/ios/cloxpickerexample/Images.xcassets/SplashScreenLegacy.imageset/image.png +0 -0
  83. package/example/ios/cloxpickerexample/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png +0 -0
  84. package/example/ios/cloxpickerexample/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png +0 -0
  85. package/example/ios/cloxpickerexample/Info.plist +82 -0
  86. package/example/ios/cloxpickerexample/PrivacyInfo.xcprivacy +48 -0
  87. package/example/ios/cloxpickerexample/SplashScreen.storyboard +48 -0
  88. package/example/ios/cloxpickerexample/Supporting/Expo.plist +12 -0
  89. package/example/ios/cloxpickerexample/cloxpickerexample-Bridging-Header.h +3 -0
  90. package/example/ios/cloxpickerexample/cloxpickerexample.entitlements +5 -0
  91. package/example/ios/cloxpickerexample.xcodeproj/project.pbxproj +552 -0
  92. package/example/ios/cloxpickerexample.xcodeproj/xcshareddata/xcschemes/cloxpickerexample.xcscheme +88 -0
  93. package/example/ios/cloxpickerexample.xcworkspace/contents.xcworkspacedata +10 -0
  94. package/example/metro.config.js +30 -0
  95. package/example/package.json +37 -0
  96. package/example/tsconfig.json +11 -0
  97. package/example/yarn.lock +5942 -0
  98. package/expo-module.config.json +9 -0
  99. package/ios/CloxPicker.podspec +29 -0
  100. package/ios/CloxPickerModule.swift +48 -0
  101. package/ios/CloxPickerView.swift +372 -0
  102. package/package.json +56 -0
  103. package/src/CloxPicker.types.ts +33 -0
  104. package/src/CloxPickerModule.ts +10 -0
  105. package/src/CloxPickerView.tsx +12 -0
  106. package/src/index.ts +13 -0
  107. package/tsconfig.json +8 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["CloxPickerModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.cloxpicker.CloxPickerModule"]
8
+ }
9
+ }
@@ -0,0 +1,29 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'CloxPicker'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platforms = {
14
+ :ios => '15.1',
15
+ :tvos => '15.1'
16
+ }
17
+ s.swift_version = '5.9'
18
+ s.source = { git: 'https://github.com/prolific-life/clox-picker' }
19
+ s.static_framework = true
20
+
21
+ s.dependency 'ExpoModulesCore'
22
+
23
+ s.pod_target_xcconfig = {
24
+ 'DEFINES_MODULE' => 'YES',
25
+ }
26
+
27
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
28
+ s.exclude_files = "build/**/*"
29
+ end
@@ -0,0 +1,48 @@
1
+ import ExpoModulesCore
2
+ import UIKit
3
+
4
+ public class CloxPickerModule: Module {
5
+ public func definition() -> ModuleDefinition {
6
+ Name("CloxPicker")
7
+
8
+ Function("getHello") { () -> String in
9
+ "Hello world"
10
+ }
11
+
12
+ Function("setColorScheme") { (scheme: String) -> Void in
13
+ DispatchQueue.main.async {
14
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
15
+ if let window = windowScene.windows.first {
16
+ switch scheme {
17
+ case "dark":
18
+ window.overrideUserInterfaceStyle = .dark
19
+ case "light":
20
+ window.overrideUserInterfaceStyle = .light
21
+ default:
22
+ window.overrideUserInterfaceStyle = .unspecified
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+
29
+ View(CloxPickerView.self) {
30
+ Prop("tabs") { (view: CloxPickerView, tabs: [[String: Any]]) in
31
+ view.setTabs(tabs)
32
+ }
33
+ Prop("height") { (view: CloxPickerView, height: Double) in
34
+ view.setHeight(height)
35
+ }
36
+ Prop("value") { (view: CloxPickerView, value: Int) in
37
+ view.setValue(value)
38
+ }
39
+ Prop("useLiquidGlass") { (view: CloxPickerView, use: Bool) in
40
+ view.setUseLiquidGlass(use)
41
+ }
42
+ Prop("selectedColor") { (view: CloxPickerView, color: String?) in
43
+ view.setSelectedColor(color)
44
+ }
45
+ Events("onTabChange")
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,372 @@
1
+ import ExpoModulesCore
2
+ import SwiftUI
3
+ import UIKit
4
+
5
+ // MARK: - Tab model
6
+ struct CloxPickerTabItem: Identifiable {
7
+ let id: Int
8
+ let name: String
9
+ let iconUri: String?
10
+ }
11
+
12
+ // MARK: - UITabBar wrapper for horizontal segmented control
13
+ class HorizontalTabBar: UITabBar {
14
+ override func layoutSubviews() {
15
+ super.layoutSubviews()
16
+
17
+ // Make tab bar horizontal instead of vertical
18
+ var xPosition: CGFloat = 0
19
+ let itemWidth = bounds.width / CGFloat(items?.count ?? 1)
20
+
21
+ for item in subviews {
22
+ if let itemView = item as? UIControl {
23
+ itemView.frame = CGRect(
24
+ x: xPosition,
25
+ y: 0,
26
+ width: itemWidth,
27
+ height: bounds.height
28
+ )
29
+ xPosition += itemWidth
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ // MARK: - UIKit Tab Bar Wrapper
36
+ struct UIKitTabBarControl: UIViewRepresentable {
37
+ let tabs: [CloxPickerTabItem]
38
+ @Binding var selectedIndex: Int
39
+ let height: CGFloat
40
+ let selectedColor: UIColor?
41
+
42
+ func makeUIView(context: Context) -> HorizontalTabBar {
43
+ let tabBar = HorizontalTabBar()
44
+ tabBar.isTranslucent = true
45
+
46
+ // Create tab bar items with icons
47
+ let tabBarItems = tabs.enumerated().map { index, tab in
48
+ let item = UITabBarItem(title: tab.name, image: nil, tag: index)
49
+
50
+ // Load icon if provided
51
+ if let iconUri = tab.iconUri {
52
+ if let url = URL(string: iconUri) {
53
+ // Load image asynchronously
54
+ URLSession.shared.dataTask(with: url) { data, _, _ in
55
+ if let data = data, let image = UIImage(data: data) {
56
+ // Use template mode so icon adapts to light/dark mode (black/white)
57
+ let renderedImage = image.withRenderingMode(.alwaysTemplate)
58
+ DispatchQueue.main.async {
59
+ item.image = renderedImage
60
+ }
61
+ }
62
+ }.resume()
63
+ } else if iconUri.hasPrefix("/") {
64
+ // Local file
65
+ if let image = UIImage(contentsOfFile: iconUri) {
66
+ item.image = image.withRenderingMode(.alwaysTemplate)
67
+ }
68
+ }
69
+ }
70
+
71
+ return item
72
+ }
73
+ tabBar.setItems(tabBarItems, animated: false)
74
+ if selectedIndex < tabBarItems.count {
75
+ tabBar.selectedItem = tabBarItems[selectedIndex]
76
+ }
77
+
78
+ // Configure appearance for iOS 26 liquid glass
79
+ if #available(iOS 26.0, *) {
80
+ // Liquid glass is automatic, but we can customize colors
81
+ let appearance = UITabBarAppearance()
82
+ appearance.configureWithTransparentBackground()
83
+
84
+ // Apply selected color if provided
85
+ if let selectedColor = selectedColor {
86
+ tabBar.tintColor = selectedColor
87
+ appearance.stackedLayoutAppearance.selected.iconColor = selectedColor
88
+ appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
89
+ .foregroundColor: selectedColor
90
+ ]
91
+ appearance.inlineLayoutAppearance.selected.iconColor = selectedColor
92
+ appearance.inlineLayoutAppearance.selected.titleTextAttributes = [
93
+ .foregroundColor: selectedColor
94
+ ]
95
+ appearance.compactInlineLayoutAppearance.selected.iconColor = selectedColor
96
+ appearance.compactInlineLayoutAppearance.selected.titleTextAttributes = [
97
+ .foregroundColor: selectedColor
98
+ ]
99
+ }
100
+
101
+ tabBar.standardAppearance = appearance
102
+ tabBar.scrollEdgeAppearance = appearance
103
+ } else {
104
+ // For iOS < 26, use tintColor
105
+ if let selectedColor = selectedColor {
106
+ tabBar.tintColor = selectedColor
107
+ }
108
+ }
109
+
110
+ tabBar.delegate = context.coordinator
111
+ return tabBar
112
+ }
113
+
114
+ func updateUIView(_ uiView: HorizontalTabBar, context: Context) {
115
+ // Update selected color if changed
116
+ if let selectedColor = selectedColor {
117
+ if #available(iOS 26.0, *) {
118
+ let appearance = uiView.standardAppearance.copy()
119
+ appearance.stackedLayoutAppearance.selected.iconColor = selectedColor
120
+ appearance.stackedLayoutAppearance.selected.titleTextAttributes = [
121
+ .foregroundColor: selectedColor
122
+ ]
123
+ appearance.inlineLayoutAppearance.selected.iconColor = selectedColor
124
+ appearance.inlineLayoutAppearance.selected.titleTextAttributes = [
125
+ .foregroundColor: selectedColor
126
+ ]
127
+ appearance.compactInlineLayoutAppearance.selected.iconColor = selectedColor
128
+ appearance.compactInlineLayoutAppearance.selected.titleTextAttributes = [
129
+ .foregroundColor: selectedColor
130
+ ]
131
+ uiView.standardAppearance = appearance
132
+ uiView.scrollEdgeAppearance = appearance
133
+ }
134
+ uiView.tintColor = selectedColor
135
+ }
136
+
137
+ // Update items if tabs changed
138
+ let currentItemCount = uiView.items?.count ?? 0
139
+ if currentItemCount != tabs.count {
140
+ let tabBarItems = tabs.enumerated().map { index, tab in
141
+ let item = UITabBarItem(title: tab.name, image: nil, tag: index)
142
+ if let iconUri = tab.iconUri {
143
+ if let url = URL(string: iconUri) {
144
+ URLSession.shared.dataTask(with: url) { data, _, _ in
145
+ if let data = data, let image = UIImage(data: data) {
146
+ // Use template mode so icon adapts to light/dark mode (black/white)
147
+ let renderedImage = image.withRenderingMode(.alwaysTemplate)
148
+ DispatchQueue.main.async {
149
+ item.image = renderedImage
150
+ }
151
+ }
152
+ }.resume()
153
+ } else if iconUri.hasPrefix("/") {
154
+ if let image = UIImage(contentsOfFile: iconUri) {
155
+ item.image = image.withRenderingMode(.alwaysTemplate)
156
+ }
157
+ }
158
+ }
159
+ return item
160
+ }
161
+ uiView.setItems(tabBarItems, animated: false)
162
+ }
163
+
164
+ // Update selection
165
+ if let items = uiView.items, selectedIndex < items.count {
166
+ uiView.selectedItem = items[selectedIndex]
167
+ }
168
+ }
169
+
170
+ func makeCoordinator() -> Coordinator {
171
+ Coordinator(self)
172
+ }
173
+
174
+ class Coordinator: NSObject, UITabBarDelegate {
175
+ var parent: UIKitTabBarControl
176
+
177
+ init(_ parent: UIKitTabBarControl) {
178
+ self.parent = parent
179
+ }
180
+
181
+ func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
182
+ parent.selectedIndex = item.tag
183
+ }
184
+ }
185
+ }
186
+
187
+ // MARK: - Segmented Picker using TabView's tab bar
188
+ struct SegmentedPickerView: View {
189
+ let tabs: [CloxPickerTabItem]
190
+ let height: CGFloat
191
+ @Binding var selectedIndex: Int
192
+ let useLiquidGlass: Bool
193
+ let selectedColor: UIColor?
194
+ let onSelectionChange: (Int) -> Void
195
+
196
+ var body: some View {
197
+ UIKitTabBarControl(
198
+ tabs: tabs,
199
+ selectedIndex: $selectedIndex,
200
+ height: height,
201
+ selectedColor: selectedColor
202
+ )
203
+ .frame(maxWidth: .infinity, minHeight: height)
204
+ .frame(height: height)
205
+ .clipped()
206
+ .onChange(of: selectedIndex) { newValue in
207
+ onSelectionChange(newValue)
208
+ }
209
+ }
210
+ }
211
+
212
+ // MARK: - CloxPickerView (ExpoView)
213
+ public class CloxPickerView: ExpoView {
214
+ private var hostingController: UIHostingController<SegmentedPickerView>?
215
+ private var currentTabs: [CloxPickerTabItem] = []
216
+ private var currentHeight: CGFloat = 44
217
+ private var currentValue: Int = 0
218
+ private var useLiquidGlass: Bool = true
219
+ private var selectedColor: UIColor?
220
+
221
+ private let onTabChange = EventDispatcher()
222
+
223
+ required init(appContext: AppContext? = nil) {
224
+ super.init(appContext: appContext)
225
+ backgroundColor = .clear
226
+ }
227
+
228
+ func setTabs(_ tabs: [[String: Any]]) {
229
+ currentTabs = tabs.compactMap { dict -> CloxPickerTabItem? in
230
+ guard let name = dict["name"] as? String,
231
+ 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)
234
+ }
235
+ if currentTabs.isEmpty {
236
+ currentTabs = [CloxPickerTabItem(id: 0, name: "Tab", iconUri: nil)]
237
+ }
238
+ updateHostingView()
239
+ }
240
+
241
+ func setHeight(_ height: Double) {
242
+ currentHeight = CGFloat(max(24, min(96, height)))
243
+ updateHostingView()
244
+ }
245
+
246
+ func setValue(_ value: Int) {
247
+ currentValue = max(0, min(value, currentTabs.count - 1))
248
+ updateHostingView()
249
+ }
250
+
251
+ func setUseLiquidGlass(_ use: Bool) {
252
+ useLiquidGlass = use
253
+ updateHostingView()
254
+ }
255
+
256
+ func setSelectedColor(_ colorString: String?) {
257
+ if let colorString = colorString {
258
+ selectedColor = UIColor.fromHex(colorString)
259
+ } else {
260
+ selectedColor = nil
261
+ }
262
+ updateHostingView()
263
+ }
264
+
265
+ private func updateHostingView() {
266
+ let binding = Binding(
267
+ get: { [weak self] in self?.currentValue ?? 0 },
268
+ set: { [weak self] newValue in
269
+ self?.currentValue = newValue
270
+ }
271
+ )
272
+ let rootView = SegmentedPickerView(
273
+ tabs: currentTabs,
274
+ height: currentHeight,
275
+ selectedIndex: binding,
276
+ useLiquidGlass: useLiquidGlass,
277
+ selectedColor: selectedColor,
278
+ onSelectionChange: { [weak self] index in
279
+ self?.currentValue = index
280
+ self?.onTabChange(["tabIndex": index])
281
+ }
282
+ )
283
+
284
+ if let existing = hostingController {
285
+ existing.rootView = rootView
286
+ return
287
+ }
288
+
289
+ let hosting = UIHostingController(rootView: rootView)
290
+ hosting.view.backgroundColor = .clear
291
+ hostingController = hosting
292
+ hosting.view.translatesAutoresizingMaskIntoConstraints = false
293
+ addSubview(hosting.view)
294
+ NSLayoutConstraint.activate([
295
+ hosting.view.topAnchor.constraint(equalTo: topAnchor),
296
+ hosting.view.leadingAnchor.constraint(equalTo: leadingAnchor),
297
+ hosting.view.trailingAnchor.constraint(equalTo: trailingAnchor),
298
+ hosting.view.bottomAnchor.constraint(equalTo: bottomAnchor),
299
+ ])
300
+ if let parentVC = findViewController() {
301
+ parentVC.addChild(hosting)
302
+ hosting.didMove(toParent: parentVC)
303
+ }
304
+ }
305
+
306
+ override public func didMoveToWindow() {
307
+ super.didMoveToWindow()
308
+ if window != nil, let hosting = hostingController, hosting.parent == nil,
309
+ let parentVC = findViewController() {
310
+ parentVC.addChild(hosting)
311
+ hosting.didMove(toParent: parentVC)
312
+ }
313
+ }
314
+
315
+ override public func layoutSubviews() {
316
+ super.layoutSubviews()
317
+ hostingController?.view.frame = bounds
318
+ }
319
+ }
320
+
321
+ extension UIView {
322
+ func findViewController() -> UIViewController? {
323
+ if let nextResponder = next as? UIViewController {
324
+ return nextResponder
325
+ }
326
+ if let nextResponder = next as? UIView {
327
+ return nextResponder.findViewController()
328
+ }
329
+ return nil
330
+ }
331
+ }
332
+
333
+ extension UIColor {
334
+ static func fromHex(_ hex: String) -> UIColor? {
335
+ var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
336
+ hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")
337
+
338
+ var rgb: UInt64 = 0
339
+
340
+ guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else {
341
+ // Try named colors
342
+ switch hexSanitized.lowercased() {
343
+ case "red": return .systemRed
344
+ case "blue": return .systemBlue
345
+ case "green": return .systemGreen
346
+ case "orange": return .systemOrange
347
+ case "purple": return .systemPurple
348
+ case "pink": return .systemPink
349
+ case "yellow": return .systemYellow
350
+ case "teal": return .systemTeal
351
+ case "indigo": return .systemIndigo
352
+ default: return nil
353
+ }
354
+ }
355
+
356
+ let length = hexSanitized.count
357
+ if length == 6 {
358
+ let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
359
+ let g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
360
+ let b = CGFloat(rgb & 0x0000FF) / 255.0
361
+ return UIColor(red: r, green: g, blue: b, alpha: 1.0)
362
+ } else if length == 8 {
363
+ let r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0
364
+ let g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0
365
+ let b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0
366
+ let a = CGFloat(rgb & 0x000000FF) / 255.0
367
+ return UIColor(red: r, green: g, blue: b, alpha: a)
368
+ }
369
+
370
+ return nil
371
+ }
372
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "clox-picker",
3
+ "version": "0.1.1",
4
+ "description": "Clox Picker native module",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "expo-module build",
9
+ "clean": "expo-module clean",
10
+ "lint": "expo-module lint",
11
+ "test": "expo-module test",
12
+ "prepare": "expo-module prepare",
13
+ "prepublishOnly": "expo-module prepublishOnly",
14
+ "expo-module": "expo-module",
15
+ "open:ios": "xed example/ios",
16
+ "open:android": "open -a \"Android Studio\" example/android",
17
+ "android": "expo run:android",
18
+ "ios": "expo run:ios"
19
+ },
20
+ "keywords": [
21
+ "react-native",
22
+ "expo",
23
+ "clox-picker",
24
+ "CloxPicker"
25
+ ],
26
+ "repository": "https://github.com/prolific-life/clox-picker",
27
+ "bugs": {
28
+ "url": "https://github.com/prolific-life/clox-picker/issues"
29
+ },
30
+ "author": "Yash Saxena <yash.saxena1217@gmail.com> (https://github.com/prolific-life/clox-picker)",
31
+ "license": "MIT",
32
+ "homepage": "https://github.com/prolific-life/clox-picker#readme",
33
+ "engines": {
34
+ "node": ">=18.14.0 || >=23.0.0"
35
+ },
36
+ "dependencies": {
37
+ "expo": "~54.0.12",
38
+ "react": "19.1.0",
39
+ "react-native": "0.81.4"
40
+ },
41
+ "devDependencies": {
42
+ "@types/react": "~19.1.0",
43
+ "@react-native-community/cli": "latest",
44
+ "expo-module-scripts": "^5.0.7"
45
+ },
46
+ "peerDependencies": {
47
+ "expo": "*",
48
+ "react": "*",
49
+ "react-native": "*"
50
+ },
51
+ "overrides": {
52
+ "glob": "^9.3.5",
53
+ "rimraf": "^5.0.0",
54
+ "pretty-format": "^29.7.0"
55
+ }
56
+ }
@@ -0,0 +1,33 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+
3
+ export type CloxPickerModuleEvents = {};
4
+
5
+ /** Single tab item for the segmented picker */
6
+ export interface CloxPickerTab {
7
+ /** Optional icon (local file or remote URL) */
8
+ icon?: { uri: string };
9
+ /** Display label */
10
+ name: string;
11
+ /** Stable id (used as key; can match index) */
12
+ id: number;
13
+ }
14
+
15
+ export interface CloxPickerViewProps {
16
+ /** Tab items (icon optional, name, id) */
17
+ tabs: CloxPickerTab[];
18
+ /** Height of the picker in points/dp */
19
+ height: number;
20
+ /** Current selected tab index */
21
+ value: number;
22
+ /** Called when user selects a different tab (passes tab index) */
23
+ onTabChange?: (event: { tabIndex: number }) => void;
24
+ /**
25
+ * iOS only: use Liquid Glass effect when available (iOS 26+).
26
+ * When false, uses the same non–liquid-glass look on all iOS versions.
27
+ * Default: true.
28
+ */
29
+ useLiquidGlass?: boolean;
30
+ /** Color for selected tab (hex string, e.g., "#007AFF" or "blue") */
31
+ selectedColor?: string;
32
+ style?: StyleProp<ViewStyle>;
33
+ }
@@ -0,0 +1,10 @@
1
+ import { NativeModule, requireNativeModule } from 'expo';
2
+
3
+ import { CloxPickerModuleEvents } from './CloxPicker.types';
4
+
5
+ declare class CloxPickerModule extends NativeModule<CloxPickerModuleEvents> {
6
+ getHello(): string;
7
+ setColorScheme(scheme: 'light' | 'dark' | 'auto'): void;
8
+ }
9
+
10
+ export default requireNativeModule<CloxPickerModule>('CloxPicker');
@@ -0,0 +1,12 @@
1
+ import { requireNativeView } from 'expo';
2
+ import * as React from 'react';
3
+
4
+ import type { CloxPickerViewProps } from './CloxPicker.types';
5
+
6
+ const NativeCloxPickerView = requireNativeView<CloxPickerViewProps>('CloxPicker');
7
+
8
+ export function CloxPickerView(props: CloxPickerViewProps) {
9
+ return <NativeCloxPickerView {...props} />;
10
+ }
11
+
12
+ export default CloxPickerView;
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ import CloxPickerModule from './CloxPickerModule';
2
+
3
+ export function getHello(): string {
4
+ return CloxPickerModule.getHello();
5
+ }
6
+
7
+ export function setColorScheme(scheme: 'light' | 'dark' | 'auto'): void {
8
+ CloxPickerModule.setColorScheme(scheme);
9
+ }
10
+
11
+ export { default as CloxPickerModule } from './CloxPickerModule';
12
+ export { CloxPickerView, default as CloxPickerViewDefault } from './CloxPickerView';
13
+ export * from './CloxPicker.types';
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "expo-module-scripts/tsconfig.base",
3
+ "compilerOptions": {
4
+ "outDir": "./build"
5
+ },
6
+ "include": ["./src"],
7
+ "exclude": ["**/__mocks__/*", "**/__tests__/*", "**/__rsc_tests__/*"]
8
+ }