@uwayss/lightui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 uwayss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # lightui
2
+
3
+ Lightweight native SwiftUI and Jetpack Compose components for React Native and Expo.
4
+
5
+ ```sh
6
+ npm install @uwayss/lightui
7
+ ```
8
+
9
+ ```tsx
10
+ import { Button, Picker, Slider } from '@uwayss/lightui';
11
+
12
+ export function Example() {
13
+ return (
14
+ <>
15
+ <Picker items={['Small', 'Medium', 'Large']} selectedIndex={1} style={{ width: 220 }} />
16
+ <Slider value={0.5} minimumValue={0} maximumValue={1} style={{ width: 220 }} />
17
+ <Button title="Continue" disabled={false} />
18
+ </>
19
+ );
20
+ }
21
+ ```
22
+
23
+ ## Documentation
24
+
25
+ - [Usage](docs/usage.md)
26
+ - [Development](docs/development.md)
@@ -0,0 +1,36 @@
1
+ buildscript {
2
+ repositories {
3
+ mavenCentral()
4
+ }
5
+ dependencies {
6
+ classpath("org.jetbrains.kotlin.plugin.compose:org.jetbrains.kotlin.plugin.compose.gradle.plugin:${kotlinVersion}")
7
+ }
8
+ }
9
+
10
+ plugins {
11
+ id 'com.android.library'
12
+ id 'expo-module-gradle-plugin'
13
+ }
14
+
15
+ apply plugin: 'org.jetbrains.kotlin.plugin.compose'
16
+
17
+ group = 'com.uwayss.lightui'
18
+ version = '0.1.0'
19
+
20
+ android {
21
+ namespace "com.uwayss.lightui"
22
+ defaultConfig {
23
+ versionCode 1
24
+ versionName "0.1.0"
25
+ }
26
+ lintOptions {
27
+ abortOnError false
28
+ }
29
+ buildFeatures {
30
+ compose true
31
+ }
32
+ }
33
+
34
+ dependencies {
35
+ implementation 'androidx.compose.material3:material3:1.3.1'
36
+ }
@@ -0,0 +1,2 @@
1
+ <manifest>
2
+ </manifest>
@@ -0,0 +1,45 @@
1
+ package com.uwayss.lightui
2
+
3
+ import android.content.Context
4
+ import androidx.compose.material3.Button
5
+ import androidx.compose.material3.MaterialTheme
6
+ import androidx.compose.material3.Text
7
+ import androidx.compose.runtime.getValue
8
+ import androidx.compose.runtime.mutableStateOf
9
+ import androidx.compose.runtime.setValue
10
+ import androidx.compose.ui.platform.ComposeView
11
+ import expo.modules.kotlin.AppContext
12
+ import expo.modules.kotlin.viewevent.EventDispatcher
13
+ import expo.modules.kotlin.views.ExpoView
14
+
15
+ class LightUIButtonView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
16
+ private val onButtonPress by EventDispatcher()
17
+ private var buttonTitle by mutableStateOf("")
18
+ private var buttonDisabled by mutableStateOf(false)
19
+
20
+ init {
21
+ addView(
22
+ ComposeView(context).apply {
23
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
24
+ setContent {
25
+ MaterialTheme {
26
+ Button(
27
+ enabled = !buttonDisabled,
28
+ onClick = { onButtonPress(emptyMap<String, Any>()) }
29
+ ) {
30
+ Text(buttonTitle)
31
+ }
32
+ }
33
+ }
34
+ }
35
+ )
36
+ }
37
+
38
+ fun setTitle(nextTitle: String) {
39
+ buttonTitle = nextTitle
40
+ }
41
+
42
+ fun setDisabled(nextDisabled: Boolean) {
43
+ buttonDisabled = nextDisabled
44
+ }
45
+ }
@@ -0,0 +1,53 @@
1
+ package com.uwayss.lightui
2
+
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+
6
+ class LightUIModule : Module() {
7
+ override fun definition() = ModuleDefinition {
8
+ Name("LightUI")
9
+
10
+ View(LightUISliderView::class) {
11
+ Name("Slider")
12
+ Events("onValueChange")
13
+
14
+ Prop("value") { view: LightUISliderView, value: Double? ->
15
+ view.setValue(value?.toFloat() ?: 0f)
16
+ }
17
+
18
+ Prop("minimumValue") { view: LightUISliderView, value: Double? ->
19
+ view.setRange(minimumValue = value?.toFloat() ?: 0f)
20
+ }
21
+
22
+ Prop("maximumValue") { view: LightUISliderView, value: Double? ->
23
+ view.setRange(maximumValue = value?.toFloat() ?: 1f)
24
+ }
25
+ }
26
+
27
+ View(LightUIPickerView::class) {
28
+ Name("Picker")
29
+ Events("onSelectionChange")
30
+
31
+ Prop("items") { view: LightUIPickerView, value: List<String>? ->
32
+ view.setItems(value.orEmpty())
33
+ }
34
+
35
+ Prop("selectedIndex") { view: LightUIPickerView, value: Int? ->
36
+ view.setSelectedIndex(value ?: 0)
37
+ }
38
+ }
39
+
40
+ View(LightUIButtonView::class) {
41
+ Name("Button")
42
+ Events("onButtonPress")
43
+
44
+ Prop("title") { view: LightUIButtonView, value: String? ->
45
+ view.setTitle(value.orEmpty())
46
+ }
47
+
48
+ Prop("disabled") { view: LightUIButtonView, value: Boolean? ->
49
+ view.setDisabled(value ?: false)
50
+ }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,115 @@
1
+ package com.uwayss.lightui
2
+
3
+ import android.content.Context
4
+ import androidx.compose.foundation.Canvas
5
+ import androidx.compose.foundation.clickable
6
+ import androidx.compose.foundation.layout.Box
7
+ import androidx.compose.foundation.layout.Row
8
+ import androidx.compose.foundation.layout.fillMaxWidth
9
+ import androidx.compose.foundation.layout.height
10
+ import androidx.compose.foundation.layout.size
11
+ import androidx.compose.foundation.layout.wrapContentSize
12
+ import androidx.compose.material3.DropdownMenuItem
13
+ import androidx.compose.material3.DropdownMenu
14
+ import androidx.compose.material3.MaterialTheme
15
+ import androidx.compose.material3.Text
16
+ import androidx.compose.runtime.getValue
17
+ import androidx.compose.runtime.mutableStateOf
18
+ import androidx.compose.runtime.setValue
19
+ import androidx.compose.ui.Alignment
20
+ import androidx.compose.ui.Modifier
21
+ import androidx.compose.ui.draw.rotate
22
+ import androidx.compose.ui.geometry.Offset
23
+ import androidx.compose.ui.graphics.StrokeCap
24
+ import androidx.compose.ui.platform.ComposeView
25
+ import androidx.compose.ui.text.font.FontWeight
26
+ import androidx.compose.ui.unit.dp
27
+ import expo.modules.kotlin.AppContext
28
+ import expo.modules.kotlin.viewevent.EventDispatcher
29
+ import expo.modules.kotlin.views.ExpoView
30
+
31
+ class LightUIPickerView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
32
+ private val onSelectionChange by EventDispatcher()
33
+ private var expanded by mutableStateOf(false)
34
+ private var pickerItems by mutableStateOf(listOf<String>())
35
+ private var pickerSelectedIndex by mutableStateOf(0)
36
+
37
+ init {
38
+ addView(
39
+ ComposeView(context).apply {
40
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
41
+ setContent {
42
+ MaterialTheme {
43
+ Box(
44
+ contentAlignment = Alignment.Center,
45
+ modifier = Modifier.fillMaxWidth()
46
+ ) {
47
+ Row(
48
+ verticalAlignment = Alignment.CenterVertically,
49
+ modifier = Modifier
50
+ .height(44.dp)
51
+ .wrapContentSize()
52
+ .clickable { expanded = true }
53
+ ) {
54
+ Text(
55
+ text = pickerItems.getOrNull(pickerSelectedIndex).orEmpty(),
56
+ color = MaterialTheme.colorScheme.primary,
57
+ fontWeight = FontWeight.SemiBold,
58
+ style = MaterialTheme.typography.headlineSmall
59
+ )
60
+
61
+ val arrowColor = MaterialTheme.colorScheme.primary
62
+ Canvas(
63
+ modifier = Modifier
64
+ .size(24.dp)
65
+ .rotate(if (expanded) 180f else 0f)
66
+ ) {
67
+ val strokeWidth = 2.5.dp.toPx()
68
+ drawLine(
69
+ arrowColor,
70
+ Offset(size.width * 0.28f, size.height * 0.4f),
71
+ Offset(size.width * 0.5f, size.height * 0.62f),
72
+ strokeWidth,
73
+ StrokeCap.Round
74
+ )
75
+ drawLine(
76
+ arrowColor,
77
+ Offset(size.width * 0.72f, size.height * 0.4f),
78
+ Offset(size.width * 0.5f, size.height * 0.62f),
79
+ strokeWidth,
80
+ StrokeCap.Round
81
+ )
82
+ }
83
+ }
84
+
85
+ DropdownMenu(
86
+ expanded = expanded,
87
+ onDismissRequest = { expanded = false }
88
+ ) {
89
+ pickerItems.forEachIndexed { index, item ->
90
+ DropdownMenuItem(
91
+ text = { Text(item) },
92
+ onClick = {
93
+ pickerSelectedIndex = index
94
+ expanded = false
95
+ onSelectionChange(mapOf("index" to index, "value" to item))
96
+ }
97
+ )
98
+ }
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+ )
105
+ }
106
+
107
+ fun setItems(nextItems: List<String>) {
108
+ pickerItems = nextItems
109
+ pickerSelectedIndex = pickerSelectedIndex.coerceIn(0, (nextItems.size - 1).coerceAtLeast(0))
110
+ }
111
+
112
+ fun setSelectedIndex(nextSelectedIndex: Int) {
113
+ pickerSelectedIndex = nextSelectedIndex.coerceIn(0, (pickerItems.size - 1).coerceAtLeast(0))
114
+ }
115
+ }
@@ -0,0 +1,58 @@
1
+ package com.uwayss.lightui
2
+
3
+ import android.content.Context
4
+ import androidx.compose.material3.MaterialTheme
5
+ import androidx.compose.material3.Slider
6
+ import androidx.compose.runtime.getValue
7
+ import androidx.compose.runtime.mutableFloatStateOf
8
+ import androidx.compose.runtime.setValue
9
+ import androidx.compose.ui.platform.ComposeView
10
+ import expo.modules.kotlin.AppContext
11
+ import expo.modules.kotlin.viewevent.EventDispatcher
12
+ import expo.modules.kotlin.views.ExpoView
13
+
14
+ class LightUISliderView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
15
+ private val onValueChange by EventDispatcher()
16
+ private var value by mutableFloatStateOf(0f)
17
+ private var minimumValue by mutableFloatStateOf(0f)
18
+ private var maximumValue by mutableFloatStateOf(1f)
19
+
20
+ init {
21
+ addView(
22
+ ComposeView(context).apply {
23
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
24
+ setContent {
25
+ MaterialTheme {
26
+ Slider(
27
+ value = value.coerceIn(minimumValue, maximumValue),
28
+ onValueChange = { setValue(it, notify = true) },
29
+ valueRange = minimumValue..maximumValue
30
+ )
31
+ }
32
+ }
33
+ }
34
+ )
35
+ }
36
+
37
+ fun setValue(nextValue: Float, notify: Boolean = false) {
38
+ val clampedValue = nextValue.coerceIn(minimumValue, maximumValue)
39
+ if (value == clampedValue) {
40
+ return
41
+ }
42
+
43
+ value = clampedValue
44
+ if (notify) {
45
+ onValueChange(mapOf("value" to clampedValue.toDouble()))
46
+ }
47
+ }
48
+
49
+ fun setRange(minimumValue: Float = this.minimumValue, maximumValue: Float = this.maximumValue) {
50
+ if (minimumValue > maximumValue) {
51
+ return
52
+ }
53
+
54
+ this.minimumValue = minimumValue
55
+ this.maximumValue = maximumValue
56
+ setValue(value)
57
+ }
58
+ }
@@ -0,0 +1,31 @@
1
+ # Development
2
+
3
+ ## Checks
4
+
5
+ Run the first-commit quality gate with:
6
+
7
+ ```sh
8
+ npm run check
9
+ ```
10
+
11
+ This runs:
12
+
13
+ - `npm run format:check`
14
+ - `npm run lint`
15
+ - `npm run typecheck`
16
+ - `npm run pack:dry`
17
+
18
+ ## Formatting
19
+
20
+ ```sh
21
+ npm run format
22
+ ```
23
+
24
+ Prettier uses the project options in `.prettierrc.json`.
25
+
26
+ ## Native Builds
27
+
28
+ Native Android and iOS build checks should be added once the repository has an
29
+ example app or CI fixture that can consume the package. For now, the baseline keeps
30
+ the package publish surface honest with formatting, linting, TypeScript, and
31
+ `npm pack --dry-run`.
package/docs/usage.md ADDED
@@ -0,0 +1,59 @@
1
+ # Usage
2
+
3
+ `@uwayss/lightui` ships small React components backed by native SwiftUI views on iOS
4
+ and Jetpack Compose views on Android.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ npm install @uwayss/lightui
10
+ ```
11
+
12
+ ## Components
13
+
14
+ ### Button
15
+
16
+ ```tsx
17
+ import { Button } from '@uwayss/lightui';
18
+
19
+ export function SaveButton() {
20
+ return <Button title="Save" disabled={false} onPress={() => {}} />;
21
+ }
22
+ ```
23
+
24
+ ### Picker
25
+
26
+ ```tsx
27
+ import { Picker } from '@uwayss/lightui';
28
+
29
+ export function SizePicker() {
30
+ return (
31
+ <Picker
32
+ items={['Small', 'Medium', 'Large']}
33
+ selectedIndex={1}
34
+ onSelectionChange={(event) => {
35
+ console.log(event.nativeEvent.index, event.nativeEvent.value);
36
+ }}
37
+ />
38
+ );
39
+ }
40
+ ```
41
+
42
+ ### Slider
43
+
44
+ ```tsx
45
+ import { Slider } from '@uwayss/lightui';
46
+
47
+ export function VolumeSlider() {
48
+ return (
49
+ <Slider
50
+ value={0.5}
51
+ minimumValue={0}
52
+ maximumValue={1}
53
+ onValueChange={(event) => {
54
+ console.log(event.nativeEvent.value);
55
+ }}
56
+ />
57
+ );
58
+ }
59
+ ```
@@ -0,0 +1,10 @@
1
+ {
2
+ "coreFeatures": ["compose"],
3
+ "platforms": ["apple", "android"],
4
+ "apple": {
5
+ "modules": ["LightUIModule"]
6
+ },
7
+ "android": {
8
+ "modules": ["com.uwayss.lightui.LightUIModule"]
9
+ }
10
+ }
@@ -0,0 +1,23 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'LightUI'
3
+ s.version = '0.1.0'
4
+ s.summary = 'Lightweight native SwiftUI components for React Native and Expo.'
5
+ s.description = 'LightUI renders small native components through Expo Modules.'
6
+ s.author = 'uwayss'
7
+ s.homepage = 'https://github.com/uwayss/lightui'
8
+ s.license = 'MIT'
9
+ s.platforms = {
10
+ :ios => '16.4',
11
+ :tvos => '16.4'
12
+ }
13
+ s.source = { git: 'https://github.com/uwayss/lightui.git', tag: s.version.to_s }
14
+ s.static_framework = true
15
+
16
+ s.dependency 'ExpoModulesCore'
17
+
18
+ s.pod_target_xcconfig = {
19
+ 'DEFINES_MODULE' => 'YES',
20
+ }
21
+
22
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
23
+ end
@@ -0,0 +1,52 @@
1
+ import ExpoModulesCore
2
+ import SwiftUI
3
+
4
+ final class LightUIButtonView: ExpoView {
5
+ private let hostingController = UIHostingController(rootView: AnyView(EmptyView()))
6
+ private let onButtonPress = EventDispatcher()
7
+ private var title = ""
8
+ private var disabled = false
9
+
10
+ required init(appContext: AppContext? = nil) {
11
+ super.init(appContext: appContext)
12
+ clipsToBounds = false
13
+ hostingController.view.backgroundColor = .clear
14
+ addSubview(hostingController.view)
15
+ render()
16
+ }
17
+
18
+ override func layoutSubviews() {
19
+ super.layoutSubviews()
20
+ hostingController.view.frame = bounds
21
+ }
22
+
23
+ func setTitle(_ nextTitle: String) {
24
+ title = nextTitle
25
+ render()
26
+ }
27
+
28
+ func setDisabled(_ nextDisabled: Bool) {
29
+ disabled = nextDisabled
30
+ render()
31
+ }
32
+
33
+ private func render() {
34
+ if #available(iOS 26.0, *) {
35
+ hostingController.rootView = AnyView(
36
+ Button(title) {
37
+ self.onButtonPress([:])
38
+ }
39
+ .buttonStyle(.glass)
40
+ .disabled(disabled)
41
+ )
42
+ } else {
43
+ hostingController.rootView = AnyView(
44
+ Button(title) {
45
+ self.onButtonPress([:])
46
+ }
47
+ .buttonStyle(.bordered)
48
+ .disabled(disabled)
49
+ )
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,50 @@
1
+ import ExpoModulesCore
2
+
3
+ public class LightUIModule: Module {
4
+ public func definition() -> ModuleDefinition {
5
+ Name("LightUI")
6
+
7
+ View(LightUISliderView.self) {
8
+ ViewName("Slider")
9
+ Events("onValueChange")
10
+
11
+ Prop("value") { (view, value: Double?) in
12
+ view.setValue(value ?? 0)
13
+ }
14
+
15
+ Prop("minimumValue") { (view, value: Double?) in
16
+ view.setRange(minimumValue: value ?? 0)
17
+ }
18
+
19
+ Prop("maximumValue") { (view, value: Double?) in
20
+ view.setRange(maximumValue: value ?? 1)
21
+ }
22
+ }
23
+
24
+ View(LightUIPickerView.self) {
25
+ ViewName("Picker")
26
+ Events("onSelectionChange")
27
+
28
+ Prop("items") { (view, value: [String]?) in
29
+ view.setItems(value ?? [])
30
+ }
31
+
32
+ Prop("selectedIndex") { (view, value: Int?) in
33
+ view.setSelectedIndex(value ?? 0)
34
+ }
35
+ }
36
+
37
+ View(LightUIButtonView.self) {
38
+ ViewName("Button")
39
+ Events("onButtonPress")
40
+
41
+ Prop("title") { (view, value: String?) in
42
+ view.setTitle(value ?? "")
43
+ }
44
+
45
+ Prop("disabled") { (view, value: Bool?) in
46
+ view.setDisabled(value ?? false)
47
+ }
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,55 @@
1
+ import ExpoModulesCore
2
+ import SwiftUI
3
+
4
+ final class LightUIPickerView: ExpoView {
5
+ private let hostingController = UIHostingController(rootView: AnyView(EmptyView()))
6
+ private let onSelectionChange = EventDispatcher()
7
+ private var items: [String] = []
8
+ private var selectedIndex = 0
9
+
10
+ required init(appContext: AppContext? = nil) {
11
+ super.init(appContext: appContext)
12
+ clipsToBounds = false
13
+ hostingController.view.backgroundColor = .clear
14
+ addSubview(hostingController.view)
15
+ render()
16
+ }
17
+
18
+ override func layoutSubviews() {
19
+ super.layoutSubviews()
20
+ hostingController.view.frame = bounds
21
+ }
22
+
23
+ func setItems(_ nextItems: [String]) {
24
+ items = nextItems
25
+ selectedIndex = min(selectedIndex, max(nextItems.count - 1, 0))
26
+ render()
27
+ }
28
+
29
+ func setSelectedIndex(_ nextSelectedIndex: Int) {
30
+ selectedIndex = min(max(nextSelectedIndex, 0), max(items.count - 1, 0))
31
+ render()
32
+ }
33
+
34
+ private func render() {
35
+ let selection = Binding<Int>(
36
+ get: { self.selectedIndex },
37
+ set: { nextIndex in
38
+ self.selectedIndex = nextIndex
39
+ self.onSelectionChange([
40
+ "index": nextIndex,
41
+ "value": self.items.indices.contains(nextIndex) ? self.items[nextIndex] : ""
42
+ ])
43
+ }
44
+ )
45
+
46
+ hostingController.rootView = AnyView(
47
+ Picker(items.indices.contains(selectedIndex) ? items[selectedIndex] : "", selection: selection) {
48
+ ForEach(Array(items.enumerated()), id: \.offset) { index, item in
49
+ Text(item).tag(index)
50
+ }
51
+ }
52
+ .pickerStyle(.menu)
53
+ )
54
+ }
55
+ }
@@ -0,0 +1,65 @@
1
+ import ExpoModulesCore
2
+ import SwiftUI
3
+
4
+ final class LightUISliderView: ExpoView {
5
+ private let hostingController = UIHostingController(rootView: AnyView(EmptyView()))
6
+ private let onValueChange = EventDispatcher()
7
+ private var value = 0.0
8
+ private var minimumValue = 0.0
9
+ private var maximumValue = 1.0
10
+
11
+ required init(appContext: AppContext? = nil) {
12
+ super.init(appContext: appContext)
13
+ clipsToBounds = true
14
+ hostingController.view.backgroundColor = .clear
15
+ addSubview(hostingController.view)
16
+ render()
17
+ }
18
+
19
+ override func layoutSubviews() {
20
+ super.layoutSubviews()
21
+ hostingController.view.frame = bounds
22
+ }
23
+
24
+ func setValue(_ nextValue: Double, notify: Bool = false) {
25
+ let clampedValue = min(max(nextValue, minimumValue), maximumValue)
26
+ guard value != clampedValue else {
27
+ return
28
+ }
29
+
30
+ value = clampedValue
31
+ render()
32
+
33
+ if notify {
34
+ onValueChange([
35
+ "value": clampedValue
36
+ ])
37
+ }
38
+ }
39
+
40
+ func setRange(minimumValue: Double? = nil, maximumValue: Double? = nil) {
41
+ let nextMinimumValue = minimumValue ?? self.minimumValue
42
+ let nextMaximumValue = maximumValue ?? self.maximumValue
43
+ guard nextMinimumValue <= nextMaximumValue else {
44
+ return
45
+ }
46
+
47
+ self.minimumValue = nextMinimumValue
48
+ self.maximumValue = nextMaximumValue
49
+ setValue(value)
50
+ render()
51
+ }
52
+
53
+ private func render() {
54
+ hostingController.rootView = AnyView(
55
+ Slider(
56
+ value: Binding(
57
+ get: { [weak self] in self?.value ?? 0 },
58
+ set: { [weak self] newValue in self?.setValue(newValue, notify: true) }
59
+ ),
60
+ in: minimumValue...maximumValue
61
+ )
62
+ .padding(.horizontal, 2)
63
+ )
64
+ }
65
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@uwayss/lightui",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight native SwiftUI and Jetpack Compose components for React Native and Expo.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/uwayss/lightui#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/uwayss/lightui.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/uwayss/lightui/issues"
13
+ },
14
+ "main": "src/index.ts",
15
+ "types": "src/index.ts",
16
+ "files": [
17
+ "LICENSE",
18
+ "android/build.gradle",
19
+ "android/src",
20
+ "docs",
21
+ "ios",
22
+ "src",
23
+ "expo-module.config.json",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "check": "npm run format:check && npm run lint && npm run typecheck && npm run pack:dry",
28
+ "format": "prettier --write .",
29
+ "format:check": "prettier --check .",
30
+ "lint": "expo lint",
31
+ "typecheck": "tsc --noEmit",
32
+ "pack:dry": "npm pack --dry-run --cache ./.npm-cache"
33
+ },
34
+ "keywords": [
35
+ "expo",
36
+ "react-native",
37
+ "swiftui",
38
+ "jetpack-compose",
39
+ "ui"
40
+ ],
41
+ "peerDependencies": {
42
+ "expo": ">=56.0.0",
43
+ "react": ">=19.0.0",
44
+ "react-native": ">=0.85.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/react": "~19.2.2",
48
+ "eslint": "^9.39.4",
49
+ "eslint-config-expo": "^56.0.4",
50
+ "eslint-config-prettier": "^10.1.8",
51
+ "eslint-import-resolver-typescript": "^4.4.5",
52
+ "eslint-plugin-prettier": "^5.5.6",
53
+ "eslint-plugin-react-compiler": "^19.1.0-rc.2",
54
+ "expo": "~56.0.12",
55
+ "expo-modules-core": "~56.0.17",
56
+ "prettier": "^3.8.4",
57
+ "react": "19.2.3",
58
+ "react-native": "0.85.3",
59
+ "typescript": "~6.0.3",
60
+ "zod-validation-error": "^4.0.2"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ }
65
+ }
package/src/Button.tsx ADDED
@@ -0,0 +1,24 @@
1
+ import { requireNativeViewManager } from 'expo-modules-core';
2
+ import * as React from 'react';
3
+ import type { NativeSyntheticEvent, ViewProps } from 'react-native';
4
+
5
+ export type ButtonPressEvent = Record<never, never>;
6
+
7
+ export type ButtonProps = ViewProps & {
8
+ title: string;
9
+ disabled?: boolean;
10
+ onPress?: (event: NativeSyntheticEvent<ButtonPressEvent>) => void;
11
+ };
12
+
13
+ type NativeButtonProps = Omit<ButtonProps, 'onPress'> & {
14
+ onButtonPress?: ButtonProps['onPress'];
15
+ };
16
+
17
+ const NativeButton: React.ComponentType<NativeButtonProps> = requireNativeViewManager(
18
+ 'LightUI',
19
+ 'Button',
20
+ );
21
+
22
+ export function Button({ onPress, ...props }: ButtonProps) {
23
+ return <NativeButton {...props} onButtonPress={onPress} />;
24
+ }
package/src/Picker.tsx ADDED
@@ -0,0 +1,23 @@
1
+ import { requireNativeViewManager } from 'expo-modules-core';
2
+ import * as React from 'react';
3
+ import type { NativeSyntheticEvent, ViewProps } from 'react-native';
4
+
5
+ export type PickerSelectionChangeEvent = {
6
+ index: number;
7
+ value: string;
8
+ };
9
+
10
+ export type PickerProps = ViewProps & {
11
+ items: string[];
12
+ selectedIndex?: number;
13
+ onSelectionChange?: (event: NativeSyntheticEvent<PickerSelectionChangeEvent>) => void;
14
+ };
15
+
16
+ const NativePicker: React.ComponentType<PickerProps> = requireNativeViewManager(
17
+ 'LightUI',
18
+ 'Picker',
19
+ );
20
+
21
+ export function Picker(props: PickerProps) {
22
+ return <NativePicker {...props} />;
23
+ }
package/src/Slider.tsx ADDED
@@ -0,0 +1,23 @@
1
+ import { requireNativeViewManager } from 'expo-modules-core';
2
+ import * as React from 'react';
3
+ import type { NativeSyntheticEvent, ViewProps } from 'react-native';
4
+
5
+ export type SliderValueChangeEvent = {
6
+ value: number;
7
+ };
8
+
9
+ export type SliderProps = ViewProps & {
10
+ value?: number;
11
+ minimumValue?: number;
12
+ maximumValue?: number;
13
+ onValueChange?: (event: NativeSyntheticEvent<SliderValueChangeEvent>) => void;
14
+ };
15
+
16
+ const NativeSlider: React.ComponentType<SliderProps> = requireNativeViewManager(
17
+ 'LightUI',
18
+ 'Slider',
19
+ );
20
+
21
+ export function Slider(props: SliderProps) {
22
+ return <NativeSlider {...props} />;
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { Button } from './Button';
2
+ export type { ButtonPressEvent, ButtonProps } from './Button';
3
+ export { Picker } from './Picker';
4
+ export type { PickerProps, PickerSelectionChangeEvent } from './Picker';
5
+ export { Slider } from './Slider';
6
+ export type { SliderProps, SliderValueChangeEvent } from './Slider';