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.
- package/.eslintrc.js +2 -0
- package/README.md +35 -0
- package/android/build.gradle +42 -0
- package/android/src/main/java/expo/modules/cloxpicker/CloxPickerModule.kt +30 -0
- package/android/src/main/java/expo/modules/cloxpicker/CloxPickerView.kt +173 -0
- package/build/CloxPicker.types.d.ts +35 -0
- package/build/CloxPicker.types.d.ts.map +1 -0
- package/build/CloxPicker.types.js +2 -0
- package/build/CloxPicker.types.js.map +1 -0
- package/build/CloxPickerModule.d.ts +9 -0
- package/build/CloxPickerModule.d.ts.map +1 -0
- package/build/CloxPickerModule.js +3 -0
- package/build/CloxPickerModule.js.map +1 -0
- package/build/CloxPickerView.d.ts +5 -0
- package/build/CloxPickerView.d.ts.map +1 -0
- package/build/CloxPickerView.js +8 -0
- package/build/CloxPickerView.js.map +1 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +11 -0
- package/build/index.js.map +1 -0
- package/example/App.tsx +158 -0
- package/example/android/app/build.gradle +182 -0
- package/example/android/app/debug.keystore +0 -0
- package/example/android/app/proguard-rules.pro +14 -0
- package/example/android/app/src/debug/AndroidManifest.xml +7 -0
- package/example/android/app/src/debugOptimized/AndroidManifest.xml +7 -0
- package/example/android/app/src/main/AndroidManifest.xml +31 -0
- package/example/android/app/src/main/java/expo/modules/cloxpicker/example/MainActivity.kt +61 -0
- package/example/android/app/src/main/java/expo/modules/cloxpicker/example/MainApplication.kt +56 -0
- package/example/android/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
- package/example/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/example/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png +0 -0
- package/example/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png +0 -0
- package/example/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png +0 -0
- package/example/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png +0 -0
- package/example/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png +0 -0
- package/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp +0 -0
- package/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp +0 -0
- package/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp +0 -0
- package/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp +0 -0
- package/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp +0 -0
- package/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- package/example/android/app/src/main/res/values/colors.xml +6 -0
- package/example/android/app/src/main/res/values/strings.xml +5 -0
- package/example/android/app/src/main/res/values/styles.xml +11 -0
- package/example/android/app/src/main/res/values-night/colors.xml +1 -0
- package/example/android/build.gradle +24 -0
- package/example/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/example/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/example/android/gradle.properties +65 -0
- package/example/android/gradlew +251 -0
- package/example/android/gradlew.bat +94 -0
- package/example/android/settings.gradle +39 -0
- package/example/app.json +30 -0
- package/example/assets/adaptive-icon.png +0 -0
- package/example/assets/favicon.png +0 -0
- package/example/assets/icon.png +0 -0
- package/example/assets/splash-icon.png +0 -0
- package/example/index.ts +5 -0
- package/example/ios/.xcode.env +11 -0
- package/example/ios/Podfile +60 -0
- package/example/ios/Podfile.lock +2211 -0
- package/example/ios/Podfile.properties.json +5 -0
- package/example/ios/cloxpickerexample/AppDelegate.swift +70 -0
- package/example/ios/cloxpickerexample/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
- package/example/ios/cloxpickerexample/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/example/ios/cloxpickerexample/Images.xcassets/Contents.json +6 -0
- package/example/ios/cloxpickerexample/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
- package/example/ios/cloxpickerexample/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +23 -0
- package/example/ios/cloxpickerexample/Images.xcassets/SplashScreenLegacy.imageset/image.png +0 -0
- package/example/ios/cloxpickerexample/Images.xcassets/SplashScreenLegacy.imageset/image@2x.png +0 -0
- package/example/ios/cloxpickerexample/Images.xcassets/SplashScreenLegacy.imageset/image@3x.png +0 -0
- package/example/ios/cloxpickerexample/Info.plist +82 -0
- package/example/ios/cloxpickerexample/PrivacyInfo.xcprivacy +48 -0
- package/example/ios/cloxpickerexample/SplashScreen.storyboard +48 -0
- package/example/ios/cloxpickerexample/Supporting/Expo.plist +12 -0
- package/example/ios/cloxpickerexample/cloxpickerexample-Bridging-Header.h +3 -0
- package/example/ios/cloxpickerexample/cloxpickerexample.entitlements +5 -0
- package/example/ios/cloxpickerexample.xcodeproj/project.pbxproj +552 -0
- package/example/ios/cloxpickerexample.xcodeproj/xcshareddata/xcschemes/cloxpickerexample.xcscheme +88 -0
- package/example/ios/cloxpickerexample.xcworkspace/contents.xcworkspacedata +10 -0
- package/example/metro.config.js +30 -0
- package/example/package.json +37 -0
- package/example/tsconfig.json +11 -0
- package/example/yarn.lock +5942 -0
- package/expo-module.config.json +9 -0
- package/ios/CloxPicker.podspec +29 -0
- package/ios/CloxPickerModule.swift +48 -0
- package/ios/CloxPickerView.swift +372 -0
- package/package.json +56 -0
- package/src/CloxPicker.types.ts +33 -0
- package/src/CloxPickerModule.ts +10 -0
- package/src/CloxPickerView.tsx +12 -0
- package/src/index.ts +13 -0
- package/tsconfig.json +8 -0
|
@@ -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';
|