diff --git a/README.md b/README.md
index d95be41..d887425 100644
--- a/README.md
+++ b/README.md
@@ -2,32 +2,24 @@
Android Material 3 components for React Native apps
-## Installation
-
-```sh
-npm install @material3/react-native
-```
-
-## Usage
+## Getting started
+From the root of the project:
-```js
-import { ReactNativeView } from "@material3/react-native";
+1. Install dependencies
-// ...
-
-
+```sh
+yarn
```
+2. Start the example app
-## Contributing
-
-See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
-
-## License
-
-MIT
+```sh
+yarn example start
+```
----
+3. Run the example app on Android
-Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
+```sh
+yarn example android
+```
diff --git a/android/build.gradle b/android/build.gradle
index 50d242f..a874502 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -111,6 +111,7 @@ dependencies {
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
implementation 'com.google.android.material:material:1.12.0'
+ implementation "me.saket.cascade:cascade:2.3.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
diff --git a/android/src/main/java/com/material3/reactnative/AlertDialogComponent.kt b/android/src/main/java/com/material3/reactnative/AlertDialogComponent.kt
new file mode 100644
index 0000000..0c66fec
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/AlertDialogComponent.kt
@@ -0,0 +1,125 @@
+package com.material3.reactnative
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+
+class AlertDialogComponent(
+ val props: ReadableMap,
+ val onPositivePress: Callback?,
+ val onNegativePress: Callback?,
+ val onNeutralPress: Callback?,
+ val onCancel: Callback?,
+ val reactContext: ReactApplicationContext,
+) : DialogFragment() {
+ private var alreadyCalled = false
+ private var alertDialog: MaterialAlertDialogBuilder? = null
+
+ fun show() {
+ val activity = reactContext.currentActivity as FragmentActivity?
+ val fragmentManager = activity!!.supportFragmentManager
+
+ this.show(fragmentManager, AlertDialogModule.NAME)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ buildDialog()
+ return alertDialog!!.create()
+ }
+
+ private fun buildDialog() {
+ alertDialog = MaterialAlertDialogBuilder(
+ requireContext(), getHeaderAlignmentTheme()
+ )
+
+ setTitle()
+ setMessage()
+ setActions()
+ setCancelable()
+ setIcon()
+ }
+
+ private fun setActions() {
+ val positiveButtonText = props.getString("positiveButtonText")
+ if (!positiveButtonText.isNullOrEmpty()) {
+ alertDialog!!.setPositiveButton(positiveButtonText) { _, _ ->
+ if (alreadyCalled) return@setPositiveButton
+
+ onPositivePress?.invoke()
+ alreadyCalled = true
+ }
+ }
+
+ val negativeButtonText = props.getString("negativeButtonText")
+ if (!negativeButtonText.isNullOrEmpty()) {
+ alertDialog!!.setNegativeButton(negativeButtonText) { _, _ ->
+ if (alreadyCalled) return@setNegativeButton
+
+ onNegativePress?.invoke()
+ alreadyCalled = true
+ }
+ }
+
+ val neutralButtonText = props.getString("neutralButtonText")
+ if (!neutralButtonText.isNullOrEmpty()) {
+ alertDialog!!.setNeutralButton(neutralButtonText) { _, _ ->
+ if (alreadyCalled) return@setNeutralButton
+
+ onNeutralPress?.invoke()
+ alreadyCalled = true
+ }
+ }
+ }
+
+ private fun setTitle() {
+ val title = props.getString("title")
+ if (title.isNullOrEmpty()) return
+
+ alertDialog!!.setTitle(title)
+ }
+
+ private fun setMessage() {
+ val message = props.getString("message")
+ if (message.isNullOrEmpty()) return
+
+ alertDialog!!.setMessage(message)
+ }
+
+ private fun setCancelable() {
+ if (props.hasKey("cancelable")) {
+ this.isCancelable = props.getBoolean("cancelable")
+ }
+ }
+
+ private fun getHeaderAlignmentTheme(): Int {
+ val headerAlignment = props.getString("headerAlignment")
+
+ return when (headerAlignment) {
+ "center" -> com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered
+ else -> com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog
+ }
+ }
+
+ private fun setIcon() {
+ val icon = props.getString("icon")
+
+ if (icon.isNullOrEmpty()) {
+ alertDialog!!.setIcon(null)
+ } else {
+ alertDialog!!.setIcon(IconHelper(alertDialog!!.context, icon).resolve())
+ }
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ if (alreadyCalled) return
+
+ onCancel?.invoke()
+ alreadyCalled = true
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/AlertDialogModule.kt b/android/src/main/java/com/material3/reactnative/AlertDialogModule.kt
new file mode 100644
index 0000000..7765080
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/AlertDialogModule.kt
@@ -0,0 +1,36 @@
+package com.material3.reactnative
+
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.UiThreadUtil
+
+class AlertDialogModule(reactContext: ReactApplicationContext) :
+ NativeAlertDialogSpec(reactContext) {
+ override fun getName() = NAME
+
+ override fun show(
+ props: ReadableMap?,
+ onPositivePress: Callback?,
+ onNegativePress: Callback?,
+ onNeutralPress: Callback?,
+ onCancel: Callback?
+ ) {
+ val alertDialog = AlertDialogComponent(
+ props = props!!,
+ onPositivePress = onPositivePress,
+ onNeutralPress = onNeutralPress,
+ reactContext = reactApplicationContext,
+ onNegativePress = onNegativePress,
+ onCancel = onCancel
+ )
+
+ UiThreadUtil.runOnUiThread {
+ alertDialog.show()
+ }
+ }
+
+ companion object {
+ const val NAME = "RTNAlertDialog"
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/ColorsModule.kt b/android/src/main/java/com/material3/reactnative/ColorsModule.kt
new file mode 100644
index 0000000..23a7281
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/ColorsModule.kt
@@ -0,0 +1,227 @@
+package com.material3.reactnative
+
+import android.content.Context
+import android.content.res.Configuration
+import android.view.ContextThemeWrapper
+import androidx.annotation.AttrRes
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.WritableMap
+import com.facebook.react.bridge.WritableNativeMap
+import com.google.android.material.color.DynamicColors
+
+class ColorsModule(reactContext: ReactApplicationContext) : NativeColorsSpec(reactContext) {
+ private var isDarkMode: Boolean = false
+ private var themedContext: Context? = null
+
+ override fun getName() = NAME
+
+ override fun getColors(): WritableMap {
+ determineContext()
+ val lightColors = resolveColors()
+ switchToDarkMode()
+ val darkColors = resolveColors()
+
+ val colors = WritableNativeMap().apply {
+ putMap("light", lightColors)
+ putMap("dark", darkColors)
+ }
+
+ return colors
+ }
+
+ companion object {
+ const val NAME = "RTNColors"
+ var DYNAMIC_COLORS_ENABLED = false
+ }
+
+ private fun resolveColors(): WritableMap {
+ val colors = WritableNativeMap().apply {
+ putString(
+ "errorContainer", getColorFromAttr(com.google.android.material.R.attr.colorErrorContainer)
+ )
+ putString(
+ "onBackground", getColorFromAttr(com.google.android.material.R.attr.colorOnBackground)
+ )
+ putString("onError", getColorFromAttr(com.google.android.material.R.attr.colorOnError))
+ putString(
+ "onErrorContainer",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnErrorContainer)
+ )
+ putString("onPrimary", getColorFromAttr(com.google.android.material.R.attr.colorOnPrimary))
+ putString(
+ "onPrimaryContainer",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnPrimaryContainer)
+ )
+ putString(
+ "onPrimaryFixed", getColorFromAttr(com.google.android.material.R.attr.colorOnPrimaryFixed)
+ )
+ putString(
+ "onPrimaryFixedVariant",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnPrimaryFixedVariant)
+ )
+ putString(
+ "onPrimarySurface",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnPrimarySurface)
+ )
+ putString(
+ "onSecondary", getColorFromAttr(com.google.android.material.R.attr.colorOnSecondary)
+ )
+ putString(
+ "onSecondaryContainer",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnSecondaryContainer)
+ )
+ putString(
+ "onSecondaryFixed",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnSecondaryFixed)
+ )
+ putString(
+ "onSecondaryFixedVariant",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnSecondaryFixedVariant)
+ )
+ putString("onSurface", getColorFromAttr(com.google.android.material.R.attr.colorOnSurface))
+ putString(
+ "onSurfaceInverse",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnSurfaceInverse)
+ )
+ putString(
+ "onSurfaceVariant",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnSurfaceVariant)
+ )
+ putString("onTertiary", getColorFromAttr(com.google.android.material.R.attr.colorOnTertiary))
+ putString(
+ "onTertiaryContainer",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnTertiaryContainer)
+ )
+ putString(
+ "onTertiaryFixed", getColorFromAttr(com.google.android.material.R.attr.colorOnTertiaryFixed)
+ )
+ putString(
+ "onTertiaryFixedVariant",
+ getColorFromAttr(com.google.android.material.R.attr.colorOnTertiaryFixedVariant)
+ )
+ putString("outline", getColorFromAttr(com.google.android.material.R.attr.colorOutline))
+ putString(
+ "outlineVariant", getColorFromAttr(com.google.android.material.R.attr.colorOutlineVariant)
+ )
+ putString(
+ "primaryContainer",
+ getColorFromAttr(com.google.android.material.R.attr.colorPrimaryContainer)
+ )
+ putString(
+ "primaryFixed", getColorFromAttr(com.google.android.material.R.attr.colorPrimaryFixed)
+ )
+ putString(
+ "primaryFixedDim", getColorFromAttr(com.google.android.material.R.attr.colorPrimaryFixedDim)
+ )
+ putString(
+ "primaryInverse", getColorFromAttr(com.google.android.material.R.attr.colorPrimaryInverse)
+ )
+ putString(
+ "primarySurface", getColorFromAttr(com.google.android.material.R.attr.colorPrimarySurface)
+ )
+ putString(
+ "primaryVariant", getColorFromAttr(com.google.android.material.R.attr.colorPrimaryVariant)
+ )
+ putString("secondary", getColorFromAttr(com.google.android.material.R.attr.colorSecondary))
+ putString(
+ "secondaryContainer",
+ getColorFromAttr(com.google.android.material.R.attr.colorSecondaryContainer)
+ )
+ putString(
+ "secondaryFixed", getColorFromAttr(com.google.android.material.R.attr.colorSecondaryFixed)
+ )
+ putString(
+ "secondaryFixedDim",
+ getColorFromAttr(com.google.android.material.R.attr.colorSecondaryFixedDim)
+ )
+ putString(
+ "secondaryVariant",
+ getColorFromAttr(com.google.android.material.R.attr.colorSecondaryVariant)
+ )
+ putString("surface", getColorFromAttr(com.google.android.material.R.attr.colorSurface))
+ putString(
+ "surfaceBright", getColorFromAttr(com.google.android.material.R.attr.colorSurfaceBright)
+ )
+ putString(
+ "surfaceContainer",
+ getColorFromAttr(com.google.android.material.R.attr.colorSurfaceContainer)
+ )
+ putString(
+ "surfaceContainerHigh",
+ getColorFromAttr(com.google.android.material.R.attr.colorSurfaceContainerHigh)
+ )
+ putString(
+ "surfaceContainerHighest",
+ getColorFromAttr(com.google.android.material.R.attr.colorSurfaceContainerHighest)
+ )
+ putString(
+ "surfaceContainerLow",
+ getColorFromAttr(com.google.android.material.R.attr.colorSurfaceContainerLow)
+ )
+ putString(
+ "surfaceContainerLowest",
+ getColorFromAttr(com.google.android.material.R.attr.colorSurfaceContainerLowest)
+ )
+ putString("surfaceDim", getColorFromAttr(com.google.android.material.R.attr.colorSurfaceDim))
+ putString(
+ "surfaceInverse", getColorFromAttr(com.google.android.material.R.attr.colorSurfaceInverse)
+ )
+ putString(
+ "surfaceVariant", getColorFromAttr(com.google.android.material.R.attr.colorSurfaceVariant)
+ )
+ putString("tertiary", getColorFromAttr(com.google.android.material.R.attr.colorTertiary))
+ putString(
+ "tertiaryContainer",
+ getColorFromAttr(com.google.android.material.R.attr.colorTertiaryContainer)
+ )
+ putString(
+ "tertiaryFixed", getColorFromAttr(com.google.android.material.R.attr.colorTertiaryFixed)
+ )
+ putString(
+ "tertiaryFixedDim",
+ getColorFromAttr(com.google.android.material.R.attr.colorTertiaryFixedDim)
+ )
+ }
+
+ return colors
+ }
+
+
+ private fun getColorFromAttr(@AttrRes attr: Int): String {
+ val typedArray = themedContext!!.theme.obtainStyledAttributes(intArrayOf(attr))
+ val color = typedArray.use {
+ it.getColor(0, 0)
+ }
+
+ val alpha = (color shr 24) and 0xFF
+ val red = (color shr 16) and 0xFF
+ val green = (color shr 8) and 0xFF
+ val blue = color and 0xFF
+
+ return "#" + red.toString(16).padStart(2, '0').uppercase() + green.toString(16).padStart(2, '0')
+ .uppercase() + blue.toString(16).padStart(2, '0').uppercase() + alpha.toString(16)
+ .padStart(2, '0').uppercase()
+ }
+
+ private fun determineContext() {
+ val newConfig = Configuration(currentActivity!!.resources.configuration).apply {
+ uiMode = if (isDarkMode) {
+ (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or Configuration.UI_MODE_NIGHT_YES
+ } else {
+ (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or Configuration.UI_MODE_NIGHT_NO
+ }
+ }
+
+ val newContext = currentActivity!!.createConfigurationContext(newConfig)
+ themedContext = ContextThemeWrapper(newContext, currentActivity!!.applicationInfo.theme)
+
+ if (DYNAMIC_COLORS_ENABLED) {
+ themedContext = DynamicColors.wrapContextIfAvailable(themedContext as ContextThemeWrapper)
+ }
+ }
+
+ private fun switchToDarkMode() {
+ isDarkMode = true
+ determineContext()
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/DatePickerComponent.kt b/android/src/main/java/com/material3/reactnative/DatePickerComponent.kt
new file mode 100644
index 0000000..b49d293
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/DatePickerComponent.kt
@@ -0,0 +1,145 @@
+package com.material3.reactnative
+
+import androidx.fragment.app.FragmentActivity
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.google.android.material.datepicker.CalendarConstraints
+import com.google.android.material.datepicker.CompositeDateValidator
+import com.google.android.material.datepicker.DateValidatorPointBackward
+import com.google.android.material.datepicker.DateValidatorPointForward
+import com.google.android.material.datepicker.MaterialDatePicker
+
+class DatePickerComponent(
+ val props: ReadableMap,
+ val onChange: Callback?,
+ val onCancel: Callback?,
+ val reactContext: ReactApplicationContext,
+ val promise: Promise
+) {
+ private val builder: MaterialDatePicker.Builder = MaterialDatePicker.Builder.datePicker()
+ private val datePicker: MaterialDatePicker
+ private var alreadyCalled = false
+
+ init {
+ setTitle()
+ setInputMode()
+ setFullscreen()
+ setButtonTexts()
+ setValue()
+ setConstraints()
+ datePicker = builder.build()
+
+ setCallbacks()
+ }
+
+ fun show() {
+ val fragmentManager = (reactContext.currentActivity as FragmentActivity).supportFragmentManager
+ datePicker.show(fragmentManager, DatePickerModule.NAME)
+ }
+
+
+ private fun setTitle() {
+ val title = props.getString("title")
+ if (title.isNullOrEmpty()) return
+
+ builder.setTitleText(title)
+ }
+
+ private fun setInputMode() {
+ val inputMode = props.getString("inputMode")
+ if (inputMode.isNullOrEmpty()) return
+
+ when (inputMode) {
+ "text" -> builder.setInputMode(MaterialDatePicker.INPUT_MODE_TEXT)
+ "calendar" -> builder.setInputMode(MaterialDatePicker.INPUT_MODE_CALENDAR)
+ else -> builder.setInputMode(MaterialDatePicker.INPUT_MODE_CALENDAR)
+ }
+ }
+
+ private fun setFullscreen() {
+ if (!props.hasKey("fullscreen")) return
+
+ val fullscreen = props.getBoolean("fullscreen")
+ if (fullscreen) {
+ builder.setTheme(com.google.android.material.R.style.ThemeOverlay_Material3_MaterialCalendar_Fullscreen)
+ }
+ }
+
+ private fun setButtonTexts() {
+ val positiveButtonText = props.getString("positiveButtonText")
+
+ if (!positiveButtonText.isNullOrEmpty()) {
+ builder.setPositiveButtonText(positiveButtonText)
+ }
+
+ val negativeButtonText = props.getString("negativeButtonText")
+
+ if (!negativeButtonText.isNullOrEmpty()) {
+ builder.setNegativeButtonText(negativeButtonText)
+ }
+ }
+
+ private fun setValue() {
+ if (!props.hasKey("value")) return
+
+ val value = props.getDouble("value")
+ builder.setSelection(value.toLong())
+ }
+
+ private fun setConstraints() {
+ val constraintsBuilder = CalendarConstraints.Builder()
+
+ if (props.hasKey("firstDayOfWeek")) {
+ constraintsBuilder.setFirstDayOfWeek(props.getInt("firstDayOfWeek"))
+ }
+
+ val validators = mutableListOf()
+
+ if (props.hasKey("minDate")) {
+ val minDate = props.getDouble("minDate").toLong()
+ validators.add(DateValidatorPointForward.from(minDate))
+ }
+
+ if (props.hasKey("maxDate")) {
+ val maxDate = props.getDouble("maxDate").toLong()
+ validators.add(DateValidatorPointBackward.before(maxDate))
+ }
+
+ constraintsBuilder.setValidator(CompositeDateValidator.allOf(validators))
+ builder.setCalendarConstraints(constraintsBuilder.build())
+ }
+
+ private fun setCallbacks() {
+ datePicker.addOnPositiveButtonClickListener {
+ if (alreadyCalled) return@addOnPositiveButtonClickListener
+
+ alreadyCalled = true
+ onChange?.invoke(it.toDouble())
+ promise.resolve(null)
+ }
+
+ datePicker.addOnCancelListener {
+ triggerCancel()
+
+ }
+
+ datePicker.addOnDismissListener {
+ triggerCancel()
+ }
+
+ datePicker.addOnNegativeButtonClickListener {
+ triggerCancel()
+ }
+ }
+
+
+ private fun triggerCancel() {
+ if (alreadyCalled) return
+
+ alreadyCalled = true
+ onCancel?.invoke()
+ promise.resolve(null)
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/DatePickerModule.kt b/android/src/main/java/com/material3/reactnative/DatePickerModule.kt
new file mode 100644
index 0000000..469137c
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/DatePickerModule.kt
@@ -0,0 +1,29 @@
+package com.material3.reactnative
+
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.UiThreadUtil
+
+class DatePickerModule(reactContext: ReactApplicationContext) : NativeDatePickerSpec(reactContext) {
+ override fun getName() = NAME
+
+ override fun show(props: ReadableMap?, onChange: Callback?, onCancel: Callback?, promise: Promise) {
+ val datePicker = DatePickerComponent(
+ props = props!!,
+ onChange = onChange,
+ onCancel = onCancel,
+ reactContext = reactApplicationContext,
+ promise = promise
+ )
+
+ UiThreadUtil.runOnUiThread {
+ datePicker.show()
+ }
+ }
+
+ companion object {
+ const val NAME = "RTNDatePicker"
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/DividerComponent.kt b/android/src/main/java/com/material3/reactnative/DividerComponent.kt
index d1d1187..cef7685 100644
--- a/android/src/main/java/com/material3/reactnative/DividerComponent.kt
+++ b/android/src/main/java/com/material3/reactnative/DividerComponent.kt
@@ -1,20 +1,20 @@
-package com.material3.reactnative
-
-import android.content.Context
-import android.util.AttributeSet
-import com.google.android.material.divider.MaterialDivider
-
-class DividerComponent : MaterialDivider {
- constructor(context: Context) : super(context)
-
- constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
-
- constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
- context, attrs, defStyleAttr
- )
-
- fun setColor(value: String?) {
- val color = value?.let { android.graphics.Color.parseColor(it) }
- if (color != null) dividerColor = color
- }
-}
+//package com.material3.reactnative
+//
+//import android.content.Context
+//import android.util.AttributeSet
+//import com.google.android.material.divider.MaterialDivider
+//
+//class DividerComponent : MaterialDivider {
+// constructor(context: Context) : super(context)
+//
+// constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+//
+// constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
+// context, attrs, defStyleAttr
+// )
+//
+// fun setColor(value: String?) {
+// val color = value?.let { android.graphics.Color.parseColor(it) }
+// if (color != null) dividerColor = color
+// }
+//}
diff --git a/android/src/main/java/com/material3/reactnative/DividerManager.kt b/android/src/main/java/com/material3/reactnative/DividerManager.kt
index a081c4d..29461c5 100644
--- a/android/src/main/java/com/material3/reactnative/DividerManager.kt
+++ b/android/src/main/java/com/material3/reactnative/DividerManager.kt
@@ -1,35 +1,35 @@
-package com.material3.reactnative
-
-import com.facebook.react.bridge.ReactApplicationContext
-import com.facebook.react.module.annotations.ReactModule
-import com.facebook.react.uimanager.SimpleViewManager
-import com.facebook.react.uimanager.ThemedReactContext
-import com.facebook.react.uimanager.ViewManagerDelegate
-import com.facebook.react.viewmanagers.RTNDividerManagerDelegate
-import com.facebook.react.viewmanagers.RTNDividerManagerInterface
-
-@ReactModule(name = DividerManager.NAME)
-class DividerManager(context: ReactApplicationContext) : SimpleViewManager(),
- RTNDividerManagerInterface {
- private val delegate: RTNDividerManagerDelegate =
- RTNDividerManagerDelegate(this)
-
- override fun getDelegate(): ViewManagerDelegate = delegate
-
- override fun getName(): String = NAME
-
- override fun createViewInstance(context: ThemedReactContext): DividerComponent =
- DividerComponent(context)
-
- companion object {
- const val NAME = "RTNDivider"
- }
-
- override fun setDividerColor(view: DividerComponent?, value: String?) {
- if (view == null) return
-
- view.setColor(value)
- }
-}
-
-
+//package com.material3.reactnative
+//
+//import com.facebook.react.bridge.ReactApplicationContext
+//import com.facebook.react.module.annotations.ReactModule
+//import com.facebook.react.uimanager.SimpleViewManager
+//import com.facebook.react.uimanager.ThemedReactContext
+//import com.facebook.react.uimanager.ViewManagerDelegate
+//import com.facebook.react.viewmanagers.RTNDividerManagerDelegate
+//import com.facebook.react.viewmanagers.RTNDividerManagerInterface
+//
+//@ReactModule(name = DividerManager.NAME)
+//class DividerManager(context: ReactApplicationContext) : SimpleViewManager(),
+// RTNDividerManagerInterface {
+// private val delegate: RTNDividerManagerDelegate =
+// RTNDividerManagerDelegate(this)
+//
+// override fun getDelegate(): ViewManagerDelegate = delegate
+//
+// override fun getName(): String = NAME
+//
+// override fun createViewInstance(context: ThemedReactContext): DividerComponent =
+// DividerComponent(context)
+//
+// companion object {
+// const val NAME = "RTNDivider"
+// }
+//
+// override fun setDividerColor(view: DividerComponent?, value: String?) {
+// if (view == null) return
+//
+// view.setColor(value)
+// }
+//}
+//
+//
diff --git a/android/src/main/java/com/material3/reactnative/IconHelper.kt b/android/src/main/java/com/material3/reactnative/IconHelper.kt
new file mode 100644
index 0000000..eebaa54
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/IconHelper.kt
@@ -0,0 +1,16 @@
+package com.material3.reactnative
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.drawable.Drawable
+import androidx.core.content.ContextCompat
+
+class IconHelper(val context: Context, val name: String) {
+ @SuppressLint("DiscouragedApi")
+ fun resolve(): Drawable? {
+ val resourceId = context.resources.getIdentifier(name, "drawable", context.packageName)
+ if (resourceId < 1) return null
+
+ return ContextCompat.getDrawable(context, resourceId)
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/Material3Package.kt b/android/src/main/java/com/material3/reactnative/Material3Package.kt
index e44b6d6..81bec57 100644
--- a/android/src/main/java/com/material3/reactnative/Material3Package.kt
+++ b/android/src/main/java/com/material3/reactnative/Material3Package.kt
@@ -1,20 +1,78 @@
package com.material3.reactnative
-import com.facebook.react.ReactPackage
+import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.module.model.ReactModuleInfo
+import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
import java.util.ArrayList
-class Material3Package : ReactPackage {
+class Material3Package : BaseReactPackage() {
override fun createViewManagers(reactContext: ReactApplicationContext): List> {
val viewManagers: MutableList> = ArrayList()
- viewManagers.add(DividerManager(reactContext))
+// viewManagers.add(DividerManager(reactContext))
return viewManagers
}
- override fun createNativeModules(reactContext: ReactApplicationContext): List {
- return emptyList()
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
+ return when (name) {
+ DatePickerModule.NAME -> DatePickerModule(reactContext)
+ TimePickerModule.NAME -> TimePickerModule(reactContext)
+ SnackbarModule.NAME -> SnackbarModule(reactContext)
+ AlertDialogModule.NAME -> AlertDialogModule(reactContext)
+ OptionsDialogModule.NAME -> OptionsDialogModule(reactContext)
+ RangePickerModule.NAME -> RangePickerModule(reactContext)
+ MenuModule.NAME -> MenuModule(reactContext)
+ ColorsModule.NAME -> ColorsModule(reactContext)
+ else -> null
+ }
+ }
+
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
+ mapOf(
+ DatePickerModule.NAME to ReactModuleInfo(
+ DatePickerModule.NAME, DatePickerModule.NAME, false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ ), TimePickerModule.NAME to ReactModuleInfo(
+ TimePickerModule.NAME, TimePickerModule.NAME, false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ ), SnackbarModule.NAME to ReactModuleInfo(
+ SnackbarModule.NAME, SnackbarModule.NAME, false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ ), AlertDialogModule.NAME to ReactModuleInfo(
+ AlertDialogModule.NAME, AlertDialogModule.NAME, false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ ), OptionsDialogModule.NAME to ReactModuleInfo(
+ OptionsDialogModule.NAME, OptionsDialogModule.NAME, false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ ), RangePickerModule.NAME to ReactModuleInfo(
+ RangePickerModule.NAME, RangePickerModule.NAME, false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ ), MenuModule.NAME to ReactModuleInfo(
+ MenuModule.NAME, MenuModule.NAME, false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ ), ColorsModule.NAME to ReactModuleInfo(
+ ColorsModule.NAME, ColorsModule.NAME, false, // canOverrideExistingModule
+ false, // needsEagerInit
+ false, // isCxxModule
+ true // isTurboModule
+ )
+ )
}
}
diff --git a/android/src/main/java/com/material3/reactnative/MenuComponent.kt b/android/src/main/java/com/material3/reactnative/MenuComponent.kt
new file mode 100644
index 0000000..958bcee
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/MenuComponent.kt
@@ -0,0 +1,171 @@
+package com.material3.reactnative
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.os.Build
+import android.view.Gravity
+import android.view.Menu
+import android.view.SubMenu
+import android.view.View
+import androidx.appcompat.widget.PopupMenu
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableArray
+import com.facebook.react.bridge.ReadableMap
+import kotlin.collections.HashMap
+
+class MenuComponent(
+ val props: ReadableMap,
+ val reactContext: ReactApplicationContext,
+ anchorView: View,
+ val currentActivity: Context,
+ val items: ReadableArray,
+ val onSelect: Callback?,
+) {
+ private val popupMenu: PopupMenu = PopupMenu(anchorView.context, anchorView);
+ private var alreadyCalled = false
+
+ init {
+ configureMenu()
+ setItemsRecursively(items.toArrayList(), null, popupMenu.menu)
+ setListener()
+ }
+
+ fun show() {
+ popupMenu.show()
+ }
+
+ private fun configureMenu() {
+ popupMenu.setForceShowIcon(true)
+ popupMenu.gravity = getGravity()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) popupMenu.menu.setGroupDividerEnabled(true)
+ }
+
+ private fun setListener() {
+ popupMenu.setOnMenuItemClickListener {
+ if (it.hasSubMenu()) return@setOnMenuItemClickListener true
+ if (alreadyCalled) return@setOnMenuItemClickListener true
+
+ onSelect?.invoke(it.itemId)
+ alreadyCalled = true
+ true
+ }
+ }
+
+ private fun setItemsRecursively(items: ArrayList?, subMenu: SubMenu?, menu: Menu?) {
+ if (items == null || items.size == 0) return
+
+ items.forEach {
+ when (it) {
+ is HashMap<*, *> -> processItemOrSubMenu(it as HashMap, subMenu, menu)
+ else -> println("Unknown type")
+ }
+ }
+ }
+
+ private fun processItemOrSubMenu(item: HashMap, subMenu: SubMenu?, menu: Menu?) {
+ val type = item["type"].toString()
+
+ when (type) {
+ "submenu" -> addSubMenu(item, subMenu, menu)
+ "item" -> addItem(item, subMenu, menu)
+ }
+ }
+
+ private fun addItem(item: HashMap, subMenu: SubMenu?, menu: Menu?) {
+ val title = item["title"]?.toString()
+ val groupId = getGroupId(item)
+ val id = getItemId(item)
+
+ val menuItem = if (menu != null) menu.add(groupId, id, Menu.NONE, title) else subMenu!!.add(
+ groupId, id, Menu.NONE, title
+ )
+
+
+ menuItem.isCheckable = getIsCheckable(item)
+ menuItem.isChecked = getIsChecked(item)
+ menuItem.icon = IconHelper(currentActivity, item["icon"].toString()).resolve()
+ }
+
+ private fun addSubMenu(item: HashMap, subMenu: SubMenu?, menu: Menu?) {
+ val title = item["title"]?.toString()
+ val groupId = getGroupId(item)
+
+ val newSubMenu = if (menu != null) menu.addSubMenu(
+ groupId, Menu.NONE, Menu.NONE, title
+ ) else subMenu!!.addSubMenu(
+ groupId, Menu.NONE, Menu.NONE, title
+ )
+
+ newSubMenu.setIcon(IconHelper(currentActivity, item["icon"].toString()).resolve())
+
+ if (!item.containsKey("items")) return
+
+ val subMenuItems = item["items"]
+ if (subMenuItems is ArrayList<*>) setItemsRecursively(
+ subMenuItems as ArrayList, newSubMenu, null
+ )
+ }
+
+ private fun getGroupId(item: HashMap): Int {
+ if (!item.containsKey("groupId")) return Menu.NONE
+
+ val groupId = item["groupId"]
+ if (groupId is Double) return groupId.toInt()
+ if (groupId is Int) return groupId
+
+ return Menu.NONE
+ }
+
+ private fun getItemId(item: HashMap): Int {
+ if (!item.containsKey("itemId")) return Menu.NONE
+
+ val itemId = item["itemId"]
+ if (itemId is Double) return itemId.toInt()
+ if (itemId is Int) return itemId
+
+ return Menu.NONE
+ }
+
+ private fun getIsChecked(item: HashMap): Boolean {
+ if (!item.containsKey("isChecked")) return false
+
+ val isChecked = item["isChecked"]
+ if (isChecked is Boolean) return isChecked
+
+ return false
+ }
+
+ private fun getIsCheckable(item: HashMap): Boolean {
+ if (!item.containsKey("isCheckable")) return false
+
+ val isCheckable = item["isCheckable"]
+ if (isCheckable is Boolean) return isCheckable
+
+ return false
+ }
+
+ private fun getGravity(): Int {
+ val gravity = props.getString("gravity")
+ return when (gravity) {
+ "top" -> Gravity.TOP
+ "bottom" -> Gravity.BOTTOM
+ "left" -> Gravity.LEFT
+ "right" -> Gravity.RIGHT
+ "center" -> Gravity.CENTER
+ "center_vertical" -> Gravity.CENTER_VERTICAL
+ "center_horizontal" -> Gravity.CENTER_HORIZONTAL
+ "fill" -> Gravity.FILL
+ "fill_vertical" -> Gravity.FILL_VERTICAL
+ "fill_horizontal" -> Gravity.FILL_HORIZONTAL
+ "start" -> Gravity.START
+ "end" -> Gravity.END
+ "clip_vertical" -> Gravity.CLIP_VERTICAL
+ "clip_horizontal" -> Gravity.CLIP_HORIZONTAL
+ "no_gravity" -> Gravity.NO_GRAVITY
+ else -> Gravity.NO_GRAVITY
+ }
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/MenuModule.kt b/android/src/main/java/com/material3/reactnative/MenuModule.kt
new file mode 100644
index 0000000..857df3c
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/MenuModule.kt
@@ -0,0 +1,35 @@
+package com.material3.reactnative
+
+import android.view.View
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableArray
+import com.facebook.react.bridge.ReadableMap
+
+class MenuModule(reactContext: ReactApplicationContext) : NativeMenuSpec(reactContext) {
+ override fun getName() = NAME
+
+ override fun show(
+ props: ReadableMap?, items: ReadableArray?, onSelect: Callback?
+ ) {
+ if (props == null || items == null) return
+
+ val id = props.getInt("id")
+ val view = currentActivity?.findViewById(id) ?: return
+
+ val menu = MenuComponent(
+ props = props,
+ reactContext = reactApplicationContext,
+ items = items,
+ anchorView = view,
+ currentActivity = currentActivity!!,
+ onSelect = onSelect
+ )
+
+ menu.show()
+ }
+
+ companion object {
+ const val NAME = "RTNMenu"
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/OptionsDialogComponent.kt b/android/src/main/java/com/material3/reactnative/OptionsDialogComponent.kt
new file mode 100644
index 0000000..103b237
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/OptionsDialogComponent.kt
@@ -0,0 +1,189 @@
+package com.material3.reactnative
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.WritableNativeArray
+import com.facebook.react.bridge.WritableNativeMap
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+
+class OptionsDialogComponent(
+ val props: ReadableMap,
+ val onPositivePress: Callback?,
+ val onNegativePress: Callback?,
+ val onNeutralPress: Callback?,
+ val onCancel: Callback?,
+ val reactContext: ReactApplicationContext,
+) : DialogFragment() {
+ private var alreadyCalled = false
+ private var alertDialog: MaterialAlertDialogBuilder? = null
+ private var pendingSelections: Array = arrayOf()
+
+ fun show() {
+ val activity = reactContext.currentActivity as FragmentActivity?
+ val fragmentManager = activity!!.supportFragmentManager
+
+ this.show(fragmentManager, AlertDialogModule.NAME)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ buildDialog()
+ return alertDialog!!.create()
+ }
+
+ private fun buildDialog() {
+ alertDialog = MaterialAlertDialogBuilder(
+ requireContext(), getHeaderAlignmentTheme()
+ )
+
+ setTitle()
+ setOptions()
+ setActions()
+ setCancelable()
+ setIcon()
+ }
+
+ private fun setActions() {
+ val positiveButtonText = props.getString("positiveButtonText")
+ if (!positiveButtonText.isNullOrEmpty()) {
+ alertDialog!!.setPositiveButton(positiveButtonText) { _, _ ->
+ if (alreadyCalled) return@setPositiveButton
+
+ onPositivePress?.invoke(getResult())
+ alreadyCalled = true
+ }
+ }
+
+ val negativeButtonText = props.getString("negativeButtonText")
+ if (!negativeButtonText.isNullOrEmpty()) {
+ alertDialog!!.setNegativeButton(negativeButtonText) { _, _ ->
+ if (alreadyCalled) return@setNegativeButton
+
+ onNegativePress?.invoke(getResult())
+ alreadyCalled = true
+ }
+ }
+
+ val neutralButtonText = props.getString("neutralButtonText")
+ if (!neutralButtonText.isNullOrEmpty()) {
+ alertDialog!!.setNeutralButton(neutralButtonText) { _, _ ->
+ if (alreadyCalled) return@setNeutralButton
+
+ onNeutralPress?.invoke(getResult())
+ alreadyCalled = true
+ }
+ }
+ }
+
+ private fun setTitle() {
+ val title = props.getString("title")
+ if (title.isNullOrEmpty()) return
+
+ alertDialog!!.setTitle(title)
+ }
+
+ private fun setOptions() {
+ if (!props.hasKey("options")) return
+
+ val options = props.getArray("options")
+ if (options == null || options.size() == 0) return
+
+ val pickerType = props.getString("pickerType")
+ val convertedOptions = options.toArrayList().map { it.toString() }.toTypedArray()
+
+ when (pickerType) {
+ "singlechoice" -> configureSingleChoice(convertedOptions)
+ "multiselect" -> configureMultiSelect(convertedOptions)
+ else -> configureRows(convertedOptions)
+ }
+ }
+
+ private fun setCancelable() {
+ if (props.hasKey("cancelable")) {
+ this.isCancelable = props.getBoolean("cancelable")
+ }
+ }
+
+ private fun getHeaderAlignmentTheme(): Int {
+ val headerAlignment = props.getString("headerAlignment")
+
+ return when (headerAlignment) {
+ "center" -> com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered
+ else -> com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog
+ }
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ if (alreadyCalled) return
+
+ onCancel?.invoke(getResult())
+ alreadyCalled = true
+ }
+
+ private fun configureRows(options: Array) {
+ alertDialog!!.setItems(options) { _, selectedIndex: Int ->
+ if (alreadyCalled) return@setItems
+
+ pendingSelections = arrayOf(selectedIndex)
+ val result = getResult()
+ onPositivePress?.invoke(result)
+ alreadyCalled = true
+ }
+ }
+
+ private fun configureSingleChoice(options: Array) {
+ val selected = props.getArray("selected")
+ val selectedIndex = if (selected == null || selected.size() == 0) -1 else selected.getInt(0)
+
+ alertDialog!!.setSingleChoiceItems(options, selectedIndex) { _, newIndex: Int ->
+ pendingSelections = arrayOf(newIndex)
+ }
+ }
+
+ private fun configureMultiSelect(options: Array) {
+ val selected = props.getArray("selected") ?: WritableNativeArray()
+ val checkedItems = BooleanArray(options.size) { false }
+
+ selected.toArrayList().forEach { selectedIndex ->
+ val parsedIndex = (selectedIndex as Double).toInt()
+ if (parsedIndex < 0 || parsedIndex >= options.size) return@forEach
+
+ checkedItems[parsedIndex] = true
+ }
+
+ alertDialog!!.setMultiChoiceItems(
+ options, checkedItems
+ ) { _, selectedIndex: Int, isChecked: Boolean ->
+ pendingSelections = if (isChecked) {
+ pendingSelections.plus(selectedIndex).distinct().sorted().toTypedArray()
+ } else {
+ pendingSelections.filter { it != selectedIndex }.distinct().sorted().toTypedArray()
+ }
+ }
+ }
+
+ private fun getResult(): WritableNativeMap {
+ val result = WritableNativeMap()
+ val selections = WritableNativeArray()
+
+ pendingSelections.forEach { selections.pushInt(it) }
+ result.putArray("selections", selections)
+
+ return result
+ }
+
+ private fun setIcon() {
+ val icon = props.getString("icon")
+
+ if (icon.isNullOrEmpty()) {
+ alertDialog!!.setIcon(null)
+ } else {
+ alertDialog!!.setIcon(IconHelper(alertDialog!!.context, icon).resolve())
+ }
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/OptionsDialogModule.kt b/android/src/main/java/com/material3/reactnative/OptionsDialogModule.kt
new file mode 100644
index 0000000..63c420d
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/OptionsDialogModule.kt
@@ -0,0 +1,36 @@
+package com.material3.reactnative
+
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.UiThreadUtil
+
+class OptionsDialogModule(reactContext: ReactApplicationContext) :
+ NativeOptionsDialogSpec(reactContext) {
+ override fun getName() = NAME
+
+ override fun show(
+ props: ReadableMap?,
+ onPositivePress: Callback?,
+ onNegativePress: Callback?,
+ onNeutralPress: Callback?,
+ onCancel: Callback?
+ ) {
+ val alertDialog = OptionsDialogComponent(
+ props = props!!,
+ onPositivePress = onPositivePress,
+ onNeutralPress = onNeutralPress,
+ reactContext = reactApplicationContext,
+ onNegativePress = onNegativePress,
+ onCancel = onCancel
+ )
+
+ UiThreadUtil.runOnUiThread {
+ alertDialog.show()
+ }
+ }
+
+ companion object {
+ const val NAME = "RTNOptionsDialog"
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/RangePickerComponent.kt b/android/src/main/java/com/material3/reactnative/RangePickerComponent.kt
new file mode 100644
index 0000000..2f6ae50
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/RangePickerComponent.kt
@@ -0,0 +1,148 @@
+package com.material3.reactnative
+
+import androidx.core.util.Pair
+import androidx.fragment.app.FragmentActivity
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.google.android.material.datepicker.CalendarConstraints
+import com.google.android.material.datepicker.CompositeDateValidator
+import com.google.android.material.datepicker.DateValidatorPointBackward
+import com.google.android.material.datepicker.DateValidatorPointForward
+import com.google.android.material.datepicker.MaterialDatePicker
+
+class RangePickerComponent(
+ val props: ReadableMap,
+ val onChange: Callback?,
+ val onCancel: Callback?,
+ val reactContext: ReactApplicationContext,
+ val promise: Promise
+) {
+ private val builder: MaterialDatePicker.Builder> =
+ MaterialDatePicker.Builder.dateRangePicker()
+ private val rangePicker: MaterialDatePicker>
+ private var alreadyCalled = false
+
+ init {
+ setTitle()
+ setInputMode()
+ setFullscreen()
+ setButtonTexts()
+ setValue()
+ setConstraints()
+ rangePicker = builder.build()
+
+ setCallbacks()
+ }
+
+ fun show() {
+ val fragmentManager = (reactContext.currentActivity as FragmentActivity).supportFragmentManager
+ rangePicker.show(fragmentManager, DatePickerModule.NAME)
+ }
+
+
+ private fun setTitle() {
+ val title = props.getString("title")
+ if (title.isNullOrEmpty()) return
+
+ builder.setTitleText(title)
+ }
+
+ private fun setInputMode() {
+ val inputMode = props.getString("inputMode")
+ if (inputMode.isNullOrEmpty()) return
+
+ when (inputMode) {
+ "text" -> builder.setInputMode(MaterialDatePicker.INPUT_MODE_TEXT)
+ "calendar" -> builder.setInputMode(MaterialDatePicker.INPUT_MODE_CALENDAR)
+ else -> builder.setInputMode(MaterialDatePicker.INPUT_MODE_CALENDAR)
+ }
+ }
+
+ private fun setFullscreen() {
+ if (!props.hasKey("fullscreen")) return
+
+ val fullscreen = props.getBoolean("fullscreen")
+ if (fullscreen) {
+ builder.setTheme(com.google.android.material.R.style.ThemeOverlay_Material3_MaterialCalendar_Fullscreen)
+ } else {
+ builder.setTheme(com.google.android.material.R.style.ThemeOverlay_Material3_MaterialCalendar)
+ }
+ }
+
+ private fun setButtonTexts() {
+ val positiveButtonText = props.getString("positiveButtonText")
+
+ if (!positiveButtonText.isNullOrEmpty()) {
+ builder.setPositiveButtonText(positiveButtonText)
+ }
+
+ val negativeButtonText = props.getString("negativeButtonText")
+
+ if (!negativeButtonText.isNullOrEmpty()) {
+ builder.setNegativeButtonText(negativeButtonText)
+ }
+ }
+
+ private fun setValue() {
+ var start: Long? = null
+ var end: Long? = null
+
+ if (props.hasKey("start")) {
+ start = props.getDouble("start").toLong()
+ }
+
+ if (props.hasKey("end")) {
+ end = props.getDouble("end").toLong()
+ }
+
+ builder.setSelection(Pair(start, end))
+ }
+
+ private fun setConstraints() {
+ val constraintsBuilder = CalendarConstraints.Builder()
+
+ if (props.hasKey("firstDayOfWeek")) {
+ constraintsBuilder.setFirstDayOfWeek(props.getInt("firstDayOfWeek"))
+ }
+
+ val validators = mutableListOf()
+
+ if (props.hasKey("minDate")) {
+ val minDate = props.getDouble("minDate").toLong()
+ validators.add(DateValidatorPointForward.from(minDate))
+ }
+
+ if (props.hasKey("maxDate")) {
+ val maxDate = props.getDouble("maxDate").toLong()
+ validators.add(DateValidatorPointBackward.before(maxDate))
+ }
+
+ constraintsBuilder.setValidator(CompositeDateValidator.allOf(validators))
+ builder.setCalendarConstraints(constraintsBuilder.build())
+ }
+
+ private fun setCallbacks() {
+ rangePicker.addOnPositiveButtonClickListener {
+ if (alreadyCalled) return@addOnPositiveButtonClickListener
+
+ alreadyCalled = true
+ onChange?.invoke(it.first.toDouble(), it.second.toDouble())
+ promise.resolve(null)
+ }
+
+ rangePicker.addOnCancelListener { triggerCancel() }
+ rangePicker.addOnDismissListener { triggerCancel() }
+ rangePicker.addOnNegativeButtonClickListener { triggerCancel() }
+ }
+
+
+ private fun triggerCancel() {
+ if (alreadyCalled) return
+
+ alreadyCalled = true
+ onCancel?.invoke()
+ promise.resolve(null)
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/RangePickerModule.kt b/android/src/main/java/com/material3/reactnative/RangePickerModule.kt
new file mode 100644
index 0000000..d4ba8bd
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/RangePickerModule.kt
@@ -0,0 +1,29 @@
+package com.material3.reactnative
+
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.UiThreadUtil
+
+class RangePickerModule(reactContext: ReactApplicationContext) : NativeDatePickerSpec(reactContext) {
+ override fun getName() = NAME
+
+ override fun show(props: ReadableMap?, onChange: Callback?, onCancel: Callback?, promise: Promise) {
+ val datePicker = RangePickerComponent(
+ props = props!!,
+ onChange = onChange,
+ onCancel = onCancel,
+ reactContext = reactApplicationContext,
+ promise = promise
+ )
+
+ UiThreadUtil.runOnUiThread {
+ datePicker.show()
+ }
+ }
+
+ companion object {
+ const val NAME = "RTNRangePicker"
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/SnackbarComponent.kt b/android/src/main/java/com/material3/reactnative/SnackbarComponent.kt
new file mode 100644
index 0000000..0cd2665
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/SnackbarComponent.kt
@@ -0,0 +1,92 @@
+package com.material3.reactnative
+
+import android.graphics.Color
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.google.android.material.snackbar.Snackbar
+
+class SnackbarComponent(
+ val props: ReadableMap,
+ private val onActionPress: Callback?,
+ val reactContext: ReactApplicationContext
+) {
+ private val snackbar: Snackbar = Snackbar.make(
+ reactContext.currentActivity!!.findViewById(android.R.id.content),
+ "",
+ Snackbar.LENGTH_SHORT
+ )
+ private var alreadyCalled = false
+
+ init {
+ setDuration()
+ setText()
+ setAnimationMode()
+ setAction()
+ setBackgroundColor()
+ }
+
+ fun show() {
+ snackbar.show()
+ }
+
+ private fun setDuration() {
+ val duration = props.getString("duration")
+
+ when (duration) {
+ "long" -> snackbar.duration = Snackbar.LENGTH_LONG
+ "indefinite" -> snackbar.duration = Snackbar.LENGTH_INDEFINITE
+ else -> snackbar.duration = Snackbar.LENGTH_SHORT
+ }
+ }
+
+ private fun setText() {
+ val text = props.getString("text")
+
+ if (text.isNullOrEmpty()) return
+ snackbar.setText(text)
+
+ if (props.hasKey("textMaxLines")) {
+ snackbar.setTextMaxLines(props.getInt("textMaxLines"))
+ }
+
+ val textColor = props.getString("textColor")
+ if (!textColor.isNullOrEmpty()) {
+ snackbar.setTextColor(android.graphics.Color.parseColor(textColor))
+ }
+ }
+
+ private fun setAnimationMode() {
+ val animationMode = props.getString("animationMode")
+
+ when (animationMode) {
+ "slide" -> snackbar.animationMode = Snackbar.ANIMATION_MODE_SLIDE
+ else -> snackbar.animationMode = Snackbar.ANIMATION_MODE_FADE
+ }
+ }
+
+ private fun setAction() {
+ val actionText = props.getString("actionText")
+
+ if (!actionText.isNullOrEmpty()) {
+ snackbar.setAction(actionText) {
+ if (alreadyCalled) return@setAction
+
+ onActionPress?.invoke()
+ alreadyCalled = true
+ }
+ }
+
+ val actionTextColor = props.getString("actionTextColor")
+ if (!actionTextColor.isNullOrEmpty()) {
+ snackbar.setActionTextColor(android.graphics.Color.parseColor(actionTextColor))
+ }
+ }
+
+ private fun setBackgroundColor() {
+ val backgroundColor = props.getString("backgroundColor")
+ if (backgroundColor.isNullOrEmpty()) return
+
+ snackbar.setBackgroundTint(Color.parseColor(backgroundColor))
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/SnackbarModule.kt b/android/src/main/java/com/material3/reactnative/SnackbarModule.kt
new file mode 100644
index 0000000..1fd2064
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/SnackbarModule.kt
@@ -0,0 +1,26 @@
+package com.material3.reactnative
+
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.UiThreadUtil
+
+class SnackbarModule(reactContext: ReactApplicationContext) : NativeSnackbarSpec(reactContext) {
+ override fun getName() = NAME
+
+ override fun show(props: ReadableMap?, onActionPress: Callback?) {
+ val snackbar = SnackbarComponent(
+ props = props!!,
+ onActionPress = onActionPress,
+ reactContext = reactApplicationContext,
+ )
+
+ UiThreadUtil.runOnUiThread {
+ snackbar.show()
+ }
+ }
+
+ companion object {
+ const val NAME = "RTNSnackbar"
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/TimePickerComponent.kt b/android/src/main/java/com/material3/reactnative/TimePickerComponent.kt
new file mode 100644
index 0000000..b2dcb8c
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/TimePickerComponent.kt
@@ -0,0 +1,134 @@
+package com.material3.reactnative
+
+import androidx.fragment.app.FragmentActivity
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.google.android.material.timepicker.MaterialTimePicker
+import android.text.format.DateFormat;
+import com.facebook.react.bridge.WritableNativeMap
+import com.google.android.material.timepicker.TimeFormat
+
+class TimePickerComponent(
+ val props: ReadableMap,
+ val onChange: Callback?,
+ val onCancel: Callback?,
+ val reactContext: ReactApplicationContext,
+ val promise: Promise
+) {
+ private val builder: MaterialTimePicker.Builder = MaterialTimePicker.Builder()
+ private val timePicker: MaterialTimePicker
+ private var alreadyCalled = false
+
+ init {
+ setTitle()
+ setInputMode()
+ setButtonTexts()
+ setValue()
+ set24HourMode()
+ timePicker = builder.build()
+
+ setCallbacks()
+ }
+
+ fun show() {
+ val fragmentManager = (reactContext.currentActivity as FragmentActivity).supportFragmentManager
+ timePicker.show(fragmentManager, TimePickerModule.NAME)
+ }
+
+
+ private fun setTitle() {
+ val title = props.getString("title")
+ if (title.isNullOrEmpty()) return
+
+ builder.setTitleText(title)
+ }
+
+ private fun setInputMode() {
+ val inputMode = props.getString("inputMode")
+ if (inputMode.isNullOrEmpty()) return
+
+ when (inputMode) {
+ "keyboard" -> builder.setInputMode(MaterialTimePicker.INPUT_MODE_KEYBOARD)
+ else -> builder.setInputMode(MaterialTimePicker.INPUT_MODE_CLOCK)
+ }
+ }
+
+ private fun setButtonTexts() {
+ val positiveButtonText = props.getString("positiveButtonText")
+
+ if (!positiveButtonText.isNullOrEmpty()) {
+ builder.setPositiveButtonText(positiveButtonText)
+ }
+
+ val negativeButtonText = props.getString("negativeButtonText")
+
+ if (!negativeButtonText.isNullOrEmpty()) {
+ builder.setNegativeButtonText(negativeButtonText)
+ }
+ }
+
+ private fun set24HourMode() {
+ var is24HourFormat = DateFormat.is24HourFormat(reactContext)
+
+ if (props.hasKey("is24HourFormat")) {
+ is24HourFormat = props.getBoolean("is24HourFormat")
+ }
+
+ if (is24HourFormat) {
+ builder.setTimeFormat(TimeFormat.CLOCK_24H)
+ } else {
+ builder.setTimeFormat(TimeFormat.CLOCK_12H)
+ }
+ }
+
+ private fun setValue() {
+
+ if (props.hasKey("hour")) {
+ builder.setHour(props.getInt("hour"))
+ }
+
+
+ if (props.hasKey("minute")) {
+ builder.setMinute(props.getInt("minute"))
+ }
+ }
+
+
+ private fun setCallbacks() {
+ timePicker.addOnPositiveButtonClickListener {
+ if (alreadyCalled) return@addOnPositiveButtonClickListener
+
+ val result = WritableNativeMap()
+ result.putInt("hour", timePicker.hour)
+ result.putInt("minute", timePicker.minute)
+ onChange?.invoke(result)
+
+ promise.resolve(null)
+ alreadyCalled = true
+ }
+
+ timePicker.addOnCancelListener {
+ triggerCancel()
+
+ }
+
+ timePicker.addOnDismissListener {
+ triggerCancel()
+ }
+
+ timePicker.addOnNegativeButtonClickListener {
+ triggerCancel()
+ }
+ }
+
+
+ private fun triggerCancel() {
+ if (alreadyCalled) return
+
+ alreadyCalled = true
+ onCancel?.invoke()
+ promise.resolve(null)
+ }
+}
diff --git a/android/src/main/java/com/material3/reactnative/TimePickerModule.kt b/android/src/main/java/com/material3/reactnative/TimePickerModule.kt
new file mode 100644
index 0000000..2c79e99
--- /dev/null
+++ b/android/src/main/java/com/material3/reactnative/TimePickerModule.kt
@@ -0,0 +1,31 @@
+package com.material3.reactnative
+
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.Promise
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReadableMap
+import com.facebook.react.bridge.UiThreadUtil
+
+class TimePickerModule(reactContext: ReactApplicationContext) : NativeTimePickerSpec(reactContext) {
+ override fun getName() = NAME
+
+ override fun show(
+ props: ReadableMap?, onChange: Callback?, onCancel: Callback?, promise: Promise
+ ) {
+ val timePicker = TimePickerComponent(
+ props = props!!,
+ onChange = onChange,
+ onCancel = onCancel,
+ reactContext = reactApplicationContext,
+ promise = promise
+ )
+
+ UiThreadUtil.runOnUiThread {
+ timePicker.show()
+ }
+ }
+
+ companion object {
+ const val NAME = "RTNTimePicker"
+ }
+}
diff --git a/android/src/main/res/drawable/twotone_alarm_24.xml b/android/src/main/res/drawable/twotone_alarm_24.xml
new file mode 100644
index 0000000..d4c2566
--- /dev/null
+++ b/android/src/main/res/drawable/twotone_alarm_24.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 5231a4c..5d9d0b7 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -110,6 +110,7 @@ android {
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
+ implementation("com.google.android.material:material:1.12.0")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
@@ -135,4 +136,4 @@ if (isNewArchitectureEnabled()) {
}
}
preBuild.dependsOn invokeLibraryCodegen
-}
\ No newline at end of file
+}
diff --git a/example/android/app/src/main/java/material3/reactnative/example/MainApplication.kt b/example/android/app/src/main/java/material3/reactnative/example/MainApplication.kt
index c4c320f..6b46186 100644
--- a/example/android/app/src/main/java/material3/reactnative/example/MainApplication.kt
+++ b/example/android/app/src/main/java/material3/reactnative/example/MainApplication.kt
@@ -1,6 +1,7 @@
package material3.reactnative.example
import android.app.Application
+import android.graphics.Color
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
@@ -11,30 +12,36 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
+import com.google.android.material.color.DynamicColors
+import com.material3.reactnative.ColorsModule
class MainApplication : Application(), ReactApplication {
- override val reactNativeHost: ReactNativeHost =
- object : DefaultReactNativeHost(this) {
- override fun getPackages(): List =
- PackageList(this).packages.apply {
- // Packages that cannot be autolinked yet can be added manually here, for example:
- // add(MyReactNativePackage())
- }
+ override val reactNativeHost: ReactNativeHost = object : DefaultReactNativeHost(this) {
+ override fun getPackages(): List = PackageList(this).packages.apply {
+ // Packages that cannot be autolinked yet can be added manually here, for example:
+ // add(MyReactNativePackage())
+ }
- override fun getJSMainModuleName(): String = "index"
+ override fun getJSMainModuleName(): String = "index"
- override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
+ override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
- override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
- override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
- }
+ override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
+ override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
+ }
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
+
+ if (DynamicColors.isDynamicColorAvailable()) {
+ DynamicColors.applyToActivitiesIfAvailable(this)
+ ColorsModule.DYNAMIC_COLORS_ENABLED = true
+ }
+
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
diff --git a/example/android/app/src/main/res/drawable/baseline_access_alarm_24.xml b/example/android/app/src/main/res/drawable/baseline_access_alarm_24.xml
new file mode 100644
index 0000000..5f48580
--- /dev/null
+++ b/example/android/app/src/main/res/drawable/baseline_access_alarm_24.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml
index 617a309..78acdc8 100644
--- a/example/android/app/src/main/res/values/styles.xml
+++ b/example/android/app/src/main/res/values/styles.xml
@@ -1,9 +1,9 @@
-
-
+
+
diff --git a/example/src/App.tsx b/example/src/App.tsx
index 1bb1636..a33484c 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,21 +1,286 @@
-import { Divider } from '@material3/react-native';
-import { View, StyleSheet } from 'react-native';
+import {
+ AlertDialog,
+ DatePicker,
+ Menu,
+ OptionsDialog,
+ Snackbar,
+ TimePicker,
+ Colors,
+ RangePicker,
+} from '@material3/react-native';
+import { forwardRef } from 'react';
+import {
+ StyleSheet,
+ ScrollView,
+ type PressableProps,
+ Pressable,
+ Text,
+} from 'react-native';
export default function App() {
return (
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
+function DatePickerButton() {
+ function show() {
+ DatePicker.show({
+ fullscreen: false,
+ inputMode: 'calendar',
+ maxDate: undefined,
+ minDate: undefined,
+ value: undefined,
+ firstDayOfWeek: 0,
+ title: 'CHOOSE A DATE',
+ negativeButtonText: 'CANCEL',
+ positiveButtonText: 'OK',
+ onChange: console.log,
+ });
+ }
+
+ return ;
+}
+
+function TimePickerButton() {
+ function show() {
+ TimePicker.show({
+ inputMode: 'clock',
+ title: 'CHOOSE A TIME',
+ negativeButtonText: 'CANCEL',
+ positiveButtonText: 'OK',
+ onChange: console.log,
+ hour: 2,
+ minute: 45,
+ is24HourFormat: false,
+ });
+ }
+
+ return ;
+}
+
+function RangePickerButton() {
+ function show() {
+ RangePicker.show({
+ title: 'CHOOSE A RANGE',
+ negativeButtonText: 'CANCEL',
+ positiveButtonText: 'OK',
+ onChange: console.log,
+ maxDate: undefined,
+ minDate: undefined,
+ value: { start: undefined, end: undefined },
+ firstDayOfWeek: 0,
+ fullscreen: true,
+ inputMode: 'calendar',
+ });
+ }
+ return ;
+}
+
+function SnackbarButton() {
+ function show() {
+ Snackbar.show({
+ text: 'Saved photos!',
+ action: {
+ text: 'UNDO',
+ onPress: () => console.log('Undo'),
+ },
+ animationMode: 'fade',
+ duration: 'short',
+ textMaxLines: 1,
+ });
+ }
+
+ return ;
+}
+
+function MenuButton() {
+ return (
+
+ );
+}
+
+function AlertDialogButton() {
+ function show() {
+ AlertDialog.show({
+ title: 'Discard draft?',
+ message: 'Your changes will be lost.',
+ negativeButtonText: 'CANCEL',
+ positiveButtonText: 'DISCARD',
+ onCancel: () => console.log('Cancel'),
+ onPositivePress: () => console.log('Confirmed'),
+ onNegativePress: () => console.log('Negative pressed'),
+ cancelable: true,
+ headerAlignment: 'start',
+ icon: 'baseline_access_alarm_24',
+ neutralButtonText: 'NEUTRAL',
+ onNeutralPress: () => console.log('Neutral pressed'),
+ });
+ }
+
+ return ;
+}
+
+function OptionsDialogButton() {
+ function show() {
+ OptionsDialog.show({
+ title: 'Next steps',
+ options: ['Go home', 'Go to work', 'Go to school'],
+ cancelable: true,
+ headerAlignment: 'start',
+ onCancel: () => console.log('Cancel'),
+ negativeButtonText: 'CANCEL',
+ onNegativePress: () => console.log('Negative pressed'),
+ neutralButtonText: 'NEUTRAL',
+ onNeutralPress: () => console.log('Neutral pressed'),
+ onPositivePress: (selectedIndex) =>
+ console.log('Option selected:', selectedIndex),
+ pickerType: 'row',
+ positiveButtonText: 'OK',
+ });
+ }
+ return ;
+}
+
+function SingleChoiceOptionsDialogButton() {
+ function show() {
+ OptionsDialog.show({
+ title: 'Choose a color',
+ options: ['Red', 'Green', 'Blue'],
+ cancelable: true,
+ headerAlignment: 'start',
+ onCancel: () => console.log('Cancel'),
+ negativeButtonText: 'CANCEL',
+ onNegativePress: () => console.log('Negative pressed'),
+ neutralButtonText: 'NEUTRAL',
+ onNeutralPress: () => console.log('Neutral pressed'),
+ onPositivePress: (selectedIndex) =>
+ console.log('Option selected:', selectedIndex),
+ pickerType: 'singlechoice',
+ positiveButtonText: 'OK',
+ icon: 'baseline_access_alarm_24',
+ });
+ }
+ return ;
+}
+
+function MultipleChoiceOptionsDialogButton() {
+ function show() {
+ OptionsDialog.show({
+ title: 'Choose your activities',
+ options: ['Biking', 'Hiking', 'Swimming', 'Running'],
+ cancelable: true,
+ headerAlignment: 'start',
+ onCancel: () => console.log('Cancel'),
+ negativeButtonText: 'CANCEL',
+ onNegativePress: () => console.log('Negative pressed'),
+ neutralButtonText: 'NEUTRAL',
+ onNeutralPress: () => console.log('Neutral pressed'),
+ onPositivePress: (selectedIndexes) =>
+ console.log('Options selected:', selectedIndexes),
+ pickerType: 'multiselect',
+ positiveButtonText: 'OK',
+ icon: 'baseline_access_alarm_24',
+ selected: [0, 1],
+ });
+ }
+ return ;
+}
+
+function PrintLightColorsButton() {
+ function show() {
+ console.log(JSON.stringify(Colors.getColors().light, null, 2));
+ }
+ return ;
+}
+
+function PrintDarkColorsButton() {
+ function show() {
+ console.log(JSON.stringify(Colors.getColors().dark, null, 2));
+ }
+ return ;
+}
+
+const Button = forwardRef(function Button(
+ { children, ...props }: PressableProps & { children: string },
+ ref
+) {
+ const primaryColor = Colors.getColors().light.primaryContainer;
+ const rippleColor = `${primaryColor}33`;
+ return (
+
+ {children}
+
+ );
+});
+
const styles = StyleSheet.create({
container: {
- paddingTop: 50,
+ flex: 1,
+ },
+ button: {
+ height: 55,
+ paddingHorizontal: 15,
+ justifyContent: 'center',
},
- box: {
- width: 60,
- height: 60,
- marginVertical: 20,
+ scrollPadding: {
+ paddingVertical: 15,
},
});
diff --git a/java/com/facebook/fbreact/specs/NativeDatePickerSpec.java b/java/com/facebook/fbreact/specs/NativeDatePickerSpec.java
new file mode 100644
index 0000000..22272b7
--- /dev/null
+++ b/java/com/facebook/fbreact/specs/NativeDatePickerSpec.java
@@ -0,0 +1,41 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateModuleJavaSpec.js
+ *
+ * @nolint
+ */
+
+package com.facebook.fbreact.specs;
+
+import com.facebook.proguard.annotations.DoNotStrip;
+import com.facebook.react.bridge.Callback;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.turbomodule.core.interfaces.TurboModule;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public abstract class NativeDatePickerSpec extends ReactContextBaseJavaModule implements TurboModule {
+ public static final String NAME = "RTNDatePicker";
+
+ public NativeDatePickerSpec(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public @Nonnull String getName() {
+ return NAME;
+ }
+
+ @ReactMethod
+ @DoNotStrip
+ public abstract void show(ReadableMap props, Callback onChange, @Nullable Callback onCancel, Promise promise);
+}
diff --git a/java/com/facebook/react/viewmanagers/RTNDividerManagerDelegate.java b/java/com/facebook/react/viewmanagers/RTNDividerManagerDelegate.java
new file mode 100644
index 0000000..dbafaea
--- /dev/null
+++ b/java/com/facebook/react/viewmanagers/RTNDividerManagerDelegate.java
@@ -0,0 +1,31 @@
+/**
+* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+*
+* Do not edit this file as changes may cause incorrect behavior and will be lost
+* once the code is regenerated.
+*
+* @generated by codegen project: GeneratePropsJavaDelegate.js
+*/
+
+package com.facebook.react.viewmanagers;
+
+import android.view.View;
+import androidx.annotation.Nullable;
+import com.facebook.react.uimanager.BaseViewManagerDelegate;
+import com.facebook.react.uimanager.BaseViewManagerInterface;
+
+public class RTNDividerManagerDelegate & RTNDividerManagerInterface> extends BaseViewManagerDelegate {
+ public RTNDividerManagerDelegate(U viewManager) {
+ super(viewManager);
+ }
+ @Override
+ public void setProperty(T view, String propName, @Nullable Object value) {
+ switch (propName) {
+ case "dividerColor":
+ mViewManager.setDividerColor(view, value == null ? null : (String) value);
+ break;
+ default:
+ super.setProperty(view, propName, value);
+ }
+ }
+}
diff --git a/java/com/facebook/react/viewmanagers/RTNDividerManagerInterface.java b/java/com/facebook/react/viewmanagers/RTNDividerManagerInterface.java
new file mode 100644
index 0000000..d37fc6c
--- /dev/null
+++ b/java/com/facebook/react/viewmanagers/RTNDividerManagerInterface.java
@@ -0,0 +1,17 @@
+/**
+* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+*
+* Do not edit this file as changes may cause incorrect behavior and will be lost
+* once the code is regenerated.
+*
+* @generated by codegen project: GeneratePropsJavaInterface.js
+*/
+
+package com.facebook.react.viewmanagers;
+
+import android.view.View;
+import androidx.annotation.Nullable;
+
+public interface RTNDividerManagerInterface {
+ void setDividerColor(T view, @Nullable String value);
+}
diff --git a/jni/CMakeLists.txt b/jni/CMakeLists.txt
new file mode 100644
index 0000000..a96fbc5
--- /dev/null
+++ b/jni/CMakeLists.txt
@@ -0,0 +1,36 @@
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under the MIT license found in the
+# LICENSE file in the root directory of this source tree.
+
+cmake_minimum_required(VERSION 3.13)
+set(CMAKE_VERBOSE_MAKEFILE on)
+
+file(GLOB react_codegen_SRCS CONFIGURE_DEPENDS *.cpp react/renderer/components/RNMaterial3Spec/*.cpp)
+
+add_library(
+ react_codegen_RNMaterial3Spec
+ OBJECT
+ ${react_codegen_SRCS}
+)
+
+target_include_directories(react_codegen_RNMaterial3Spec PUBLIC . react/renderer/components/RNMaterial3Spec)
+
+target_link_libraries(
+ react_codegen_RNMaterial3Spec
+ fbjni
+ jsi
+ # We need to link different libraries based on whether we are building rncore or not, that's necessary
+ # because we want to break a circular dependency between react_codegen_rncore and reactnative
+ reactnative
+)
+
+target_compile_options(
+ react_codegen_RNMaterial3Spec
+ PRIVATE
+ -DLOG_TAG=\"ReactNative\"
+ -fexceptions
+ -frtti
+ -std=c++20
+ -Wall
+)
diff --git a/jni/RNMaterial3Spec-generated.cpp b/jni/RNMaterial3Spec-generated.cpp
new file mode 100644
index 0000000..6c21714
--- /dev/null
+++ b/jni/RNMaterial3Spec-generated.cpp
@@ -0,0 +1,32 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateModuleJniCpp.js
+ */
+
+#include "RNMaterial3Spec.h"
+
+namespace facebook::react {
+
+static facebook::jsi::Value __hostFunction_NativeDatePickerSpecJSI_show(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
+ static jmethodID cachedMethodId = nullptr;
+ return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, "show", "(Lcom/facebook/react/bridge/ReadableMap;Lcom/facebook/react/bridge/Callback;Lcom/facebook/react/bridge/Callback;Lcom/facebook/react/bridge/Promise;)V", args, count, cachedMethodId);
+}
+
+NativeDatePickerSpecJSI::NativeDatePickerSpecJSI(const JavaTurboModule::InitParams ¶ms)
+ : JavaTurboModule(params) {
+ methodMap_["show"] = MethodMetadata {3, __hostFunction_NativeDatePickerSpecJSI_show};
+}
+
+std::shared_ptr RNMaterial3Spec_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms) {
+ if (moduleName == "RTNDatePicker") {
+ return std::make_shared(params);
+ }
+ return nullptr;
+}
+
+} // namespace facebook::react
diff --git a/jni/RNMaterial3Spec.h b/jni/RNMaterial3Spec.h
new file mode 100644
index 0000000..071e830
--- /dev/null
+++ b/jni/RNMaterial3Spec.h
@@ -0,0 +1,31 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateModuleJniH.js
+ */
+
+#pragma once
+
+#include
+#include
+#include
+
+namespace facebook::react {
+
+/**
+ * JNI C++ class for module 'NativeDatePicker'
+ */
+class JSI_EXPORT NativeDatePickerSpecJSI : public JavaTurboModule {
+public:
+ NativeDatePickerSpecJSI(const JavaTurboModule::InitParams ¶ms);
+};
+
+
+JSI_EXPORT
+std::shared_ptr RNMaterial3Spec_ModuleProvider(const std::string &moduleName, const JavaTurboModule::InitParams ¶ms);
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/ComponentDescriptors.cpp b/jni/react/renderer/components/RNMaterial3Spec/ComponentDescriptors.cpp
new file mode 100644
index 0000000..1011ca7
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/ComponentDescriptors.cpp
@@ -0,0 +1,22 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateComponentDescriptorCpp.js
+ */
+
+#include "ComponentDescriptors.h"
+#include
+#include
+
+namespace facebook::react {
+
+void RNMaterial3Spec_registerComponentDescriptorsFromCodegen(
+ std::shared_ptr registry) {
+registry->add(concreteComponentDescriptorProvider());
+}
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/ComponentDescriptors.h b/jni/react/renderer/components/RNMaterial3Spec/ComponentDescriptors.h
new file mode 100644
index 0000000..7dcb42e
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/ComponentDescriptors.h
@@ -0,0 +1,24 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateComponentDescriptorH.js
+ */
+
+#pragma once
+
+#include "ShadowNodes.h"
+#include
+#include
+
+namespace facebook::react {
+
+using RTNDividerComponentDescriptor = ConcreteComponentDescriptor;
+
+void RNMaterial3Spec_registerComponentDescriptorsFromCodegen(
+ std::shared_ptr registry);
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/EventEmitters.cpp b/jni/react/renderer/components/RNMaterial3Spec/EventEmitters.cpp
new file mode 100644
index 0000000..aef8741
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/EventEmitters.cpp
@@ -0,0 +1,16 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateEventEmitterCpp.js
+ */
+
+#include "EventEmitters.h"
+
+
+namespace facebook::react {
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/EventEmitters.h b/jni/react/renderer/components/RNMaterial3Spec/EventEmitters.h
new file mode 100644
index 0000000..e8da5fd
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/EventEmitters.h
@@ -0,0 +1,23 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateEventEmitterH.js
+ */
+#pragma once
+
+#include
+
+
+namespace facebook::react {
+class RTNDividerEventEmitter : public ViewEventEmitter {
+ public:
+ using ViewEventEmitter::ViewEventEmitter;
+
+
+
+};
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/Props.cpp b/jni/react/renderer/components/RNMaterial3Spec/Props.cpp
new file mode 100644
index 0000000..16accb0
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/Props.cpp
@@ -0,0 +1,25 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GeneratePropsCpp.js
+ */
+
+#include "Props.h"
+#include
+#include
+
+namespace facebook::react {
+
+RTNDividerProps::RTNDividerProps(
+ const PropsParserContext &context,
+ const RTNDividerProps &sourceProps,
+ const RawProps &rawProps): ViewProps(context, sourceProps, rawProps),
+
+ dividerColor(convertRawProp(context, rawProps, "dividerColor", sourceProps.dividerColor, {}))
+ {}
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/Props.h b/jni/react/renderer/components/RNMaterial3Spec/Props.h
new file mode 100644
index 0000000..4c7faef
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/Props.h
@@ -0,0 +1,27 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GeneratePropsH.js
+ */
+#pragma once
+
+#include
+#include
+
+namespace facebook::react {
+
+class RTNDividerProps final : public ViewProps {
+ public:
+ RTNDividerProps() = default;
+ RTNDividerProps(const PropsParserContext& context, const RTNDividerProps &sourceProps, const RawProps &rawProps);
+
+#pragma mark - Props
+
+ std::string dividerColor{};
+};
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/RNMaterial3SpecJSI-generated.cpp b/jni/react/renderer/components/RNMaterial3Spec/RNMaterial3SpecJSI-generated.cpp
new file mode 100644
index 0000000..421849a
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/RNMaterial3SpecJSI-generated.cpp
@@ -0,0 +1,29 @@
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateModuleCpp.js
+ */
+
+#include "RNMaterial3SpecJSI.h"
+
+namespace facebook::react {
+
+static jsi::Value __hostFunction_NativeDatePickerCxxSpecJSI_show(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
+ return static_cast(&turboModule)->show(
+ rt,
+ count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asObject(rt),
+ count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asObject(rt).asFunction(rt),
+ count <= 2 || args[2].isUndefined() ? std::nullopt : std::make_optional(args[2].asObject(rt).asFunction(rt))
+ );
+}
+
+NativeDatePickerCxxSpecJSI::NativeDatePickerCxxSpecJSI(std::shared_ptr jsInvoker)
+ : TurboModule("RTNDatePicker", jsInvoker) {
+ methodMap_["show"] = MethodMetadata {3, __hostFunction_NativeDatePickerCxxSpecJSI_show};
+}
+
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/RNMaterial3SpecJSI.h b/jni/react/renderer/components/RNMaterial3Spec/RNMaterial3SpecJSI.h
new file mode 100644
index 0000000..acde4ca
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/RNMaterial3SpecJSI.h
@@ -0,0 +1,67 @@
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateModuleH.js
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace facebook::react {
+
+
+ class JSI_EXPORT NativeDatePickerCxxSpecJSI : public TurboModule {
+protected:
+ NativeDatePickerCxxSpecJSI(std::shared_ptr jsInvoker);
+
+public:
+ virtual jsi::Value show(jsi::Runtime &rt, jsi::Object props, jsi::Function onChange, std::optional onCancel) = 0;
+
+};
+
+template
+class JSI_EXPORT NativeDatePickerCxxSpec : public TurboModule {
+public:
+ jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override {
+ return delegate_.get(rt, propName);
+ }
+
+ static constexpr std::string_view kModuleName = "RTNDatePicker";
+
+protected:
+ NativeDatePickerCxxSpec(std::shared_ptr jsInvoker)
+ : TurboModule(std::string{NativeDatePickerCxxSpec::kModuleName}, jsInvoker),
+ delegate_(reinterpret_cast(this), jsInvoker) {}
+
+
+private:
+ class Delegate : public NativeDatePickerCxxSpecJSI {
+ public:
+ Delegate(T *instance, std::shared_ptr jsInvoker) :
+ NativeDatePickerCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {
+
+ }
+
+ jsi::Value show(jsi::Runtime &rt, jsi::Object props, jsi::Function onChange, std::optional onCancel) override {
+ static_assert(
+ bridging::getParameterCount(&T::show) == 4,
+ "Expected show(...) to have 4 parameters");
+
+ return bridging::callFromJs(
+ rt, &T::show, jsInvoker_, instance_, std::move(props), std::move(onChange), std::move(onCancel));
+ }
+
+ private:
+ friend class NativeDatePickerCxxSpec;
+ T *instance_;
+ };
+
+ Delegate delegate_;
+};
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/ShadowNodes.cpp b/jni/react/renderer/components/RNMaterial3Spec/ShadowNodes.cpp
new file mode 100644
index 0000000..5126003
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/ShadowNodes.cpp
@@ -0,0 +1,17 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateShadowNodeCpp.js
+ */
+
+#include "ShadowNodes.h"
+
+namespace facebook::react {
+
+extern const char RTNDividerComponentName[] = "RTNDivider";
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/ShadowNodes.h b/jni/react/renderer/components/RNMaterial3Spec/ShadowNodes.h
new file mode 100644
index 0000000..64f9d84
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/ShadowNodes.h
@@ -0,0 +1,32 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateShadowNodeH.js
+ */
+
+#pragma once
+
+#include "EventEmitters.h"
+#include "Props.h"
+#include "States.h"
+#include
+#include
+
+namespace facebook::react {
+
+JSI_EXPORT extern const char RTNDividerComponentName[];
+
+/*
+ * `ShadowNode` for component.
+ */
+using RTNDividerShadowNode = ConcreteViewShadowNode<
+ RTNDividerComponentName,
+ RTNDividerProps,
+ RTNDividerEventEmitter,
+ RTNDividerState>;
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/States.cpp b/jni/react/renderer/components/RNMaterial3Spec/States.cpp
new file mode 100644
index 0000000..1dbb184
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/States.cpp
@@ -0,0 +1,16 @@
+
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateStateCpp.js
+ */
+#include "States.h"
+
+namespace facebook::react {
+
+
+
+} // namespace facebook::react
diff --git a/jni/react/renderer/components/RNMaterial3Spec/States.h b/jni/react/renderer/components/RNMaterial3Spec/States.h
new file mode 100644
index 0000000..0604eb6
--- /dev/null
+++ b/jni/react/renderer/components/RNMaterial3Spec/States.h
@@ -0,0 +1,29 @@
+/**
+ * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
+ *
+ * Do not edit this file as changes may cause incorrect behavior and will be lost
+ * once the code is regenerated.
+ *
+ * @generated by codegen project: GenerateStateH.js
+ */
+#pragma once
+
+#ifdef ANDROID
+#include
+#endif
+
+namespace facebook::react {
+
+class RTNDividerState {
+public:
+ RTNDividerState() = default;
+
+#ifdef ANDROID
+ RTNDividerState(RTNDividerState const &previousState, folly::dynamic data){};
+ folly::dynamic getDynamic() const {
+ return {};
+ };
+#endif
+};
+
+} // namespace facebook::react
\ No newline at end of file
diff --git a/lefthook.yml b/lefthook.yml
new file mode 100644
index 0000000..f6e5dfe
--- /dev/null
+++ b/lefthook.yml
@@ -0,0 +1,35 @@
+# EXAMPLE USAGE:
+#
+# Refer for explanation to following link:
+# https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md
+#
+# pre-push:
+# commands:
+# packages-audit:
+# tags: frontend security
+# run: yarn audit
+# gems-audit:
+# tags: backend security
+# run: bundle audit
+#
+# pre-commit:
+# parallel: true
+# commands:
+# eslint:
+# glob: "*.{js,ts,jsx,tsx}"
+# run: yarn eslint {staged_files}
+# rubocop:
+# tags: backend style
+# glob: "*.rb"
+# exclude: '(^|/)(application|routes)\.rb$'
+# run: bundle exec rubocop --force-exclusion {all_files}
+# govet:
+# tags: backend style
+# files: git ls-files -m
+# glob: "*.go"
+# run: go vet {files}
+# scripts:
+# "hello.js":
+# runner: node
+# "any.go":
+# runner: go run
diff --git a/package.json b/package.json
index c0c5212..16a1000 100644
--- a/package.json
+++ b/package.json
@@ -184,7 +184,7 @@
},
"codegenConfig": {
"name": "RNMaterial3Spec",
- "type": "all",
+ "type": "modules",
"jsSrcsDir": "src/specs",
"outputDir": {
"ios": "ios/generated",
diff --git a/src/alert_dialog/alert_dialog.ios.ts b/src/alert_dialog/alert_dialog.ios.ts
new file mode 100644
index 0000000..b5591be
--- /dev/null
+++ b/src/alert_dialog/alert_dialog.ios.ts
@@ -0,0 +1,3 @@
+export const AlertDialog = {
+ show: () => {},
+};
diff --git a/src/alert_dialog/alert_dialog.ts b/src/alert_dialog/alert_dialog.ts
new file mode 100644
index 0000000..25c93cb
--- /dev/null
+++ b/src/alert_dialog/alert_dialog.ts
@@ -0,0 +1,29 @@
+import NativeAlertDialog, { type ShowProps } from '../specs/NativeAlertDialog';
+
+function show({
+ onNegativePress,
+ onPositivePress,
+ onNeutralPress,
+ onCancel,
+ ...props
+}: AlertDialogProps) {
+ NativeAlertDialog.show(
+ props,
+ onPositivePress,
+ onNegativePress,
+ onNeutralPress,
+ onCancel
+ );
+}
+
+export const AlertDialog = {
+ show,
+};
+
+export type AlertDialogProps = Omit & {
+ onPositivePress?: () => void;
+ onNegativePress?: () => void;
+ onNeutralPress?: () => void;
+ onCancel?: () => void;
+ headerAlignment?: 'start' | 'center';
+};
diff --git a/src/alert_dialog/index.ts b/src/alert_dialog/index.ts
new file mode 100644
index 0000000..5291ec8
--- /dev/null
+++ b/src/alert_dialog/index.ts
@@ -0,0 +1,2 @@
+export { AlertDialog } from './alert_dialog';
+export type { AlertDialogProps } from './alert_dialog';
diff --git a/src/colors/colors.ios.ts b/src/colors/colors.ios.ts
new file mode 100644
index 0000000..7e81551
--- /dev/null
+++ b/src/colors/colors.ios.ts
@@ -0,0 +1,3 @@
+export const Colors = {
+ getColors: () => {},
+};
diff --git a/src/colors/colors.ts b/src/colors/colors.ts
new file mode 100644
index 0000000..09d0720
--- /dev/null
+++ b/src/colors/colors.ts
@@ -0,0 +1,13 @@
+import NativeColors, { type MaterialThemes } from '../specs/NativeColors';
+
+let cachedColors: MaterialThemes;
+
+export const Colors = {
+ getColors: () => {
+ if (!cachedColors) {
+ cachedColors = NativeColors.getColors();
+ }
+
+ return cachedColors;
+ },
+};
diff --git a/src/colors/index.ts b/src/colors/index.ts
new file mode 100644
index 0000000..d6f5c26
--- /dev/null
+++ b/src/colors/index.ts
@@ -0,0 +1,2 @@
+export { Colors } from './colors';
+export type { MaterialThemes, MaterialColors } from '../specs/NativeColors';
diff --git a/src/datepicker/datepicker.ios.ts b/src/datepicker/datepicker.ios.ts
new file mode 100644
index 0000000..c6ab8ad
--- /dev/null
+++ b/src/datepicker/datepicker.ios.ts
@@ -0,0 +1,3 @@
+export const DatePicker = {
+ show: () => {},
+};
diff --git a/src/datepicker/datepicker.ts b/src/datepicker/datepicker.ts
new file mode 100644
index 0000000..e2faed4
--- /dev/null
+++ b/src/datepicker/datepicker.ts
@@ -0,0 +1,41 @@
+import NativeDatePicker, { type ShowProps } from '../specs/NativeDatePicker';
+
+function show({
+ onChange,
+ onCancel,
+ value,
+ maxDate,
+ minDate,
+ ...props
+}: DatePickerProps): Promise {
+ function handleChange(newTimestamp: number) {
+ onChange(new Date(newTimestamp));
+ }
+
+ return NativeDatePicker.show(
+ {
+ value: value?.getTime(),
+ maxDate: maxDate?.getTime(),
+ minDate: minDate?.getTime(),
+ ...props,
+ },
+ handleChange,
+ onCancel
+ );
+}
+
+export const DatePicker = {
+ show,
+};
+
+export type DatePickerProps = Omit<
+ ShowProps,
+ 'value' | 'inputMode' | 'maxDate' | 'minDate'
+> & {
+ onChange: (newValue: Date) => void;
+ onCancel?: () => void;
+ value?: Date;
+ maxDate?: Date;
+ minDate?: Date;
+ inputMode?: 'text' | 'calendar';
+};
diff --git a/src/datepicker/index.ts b/src/datepicker/index.ts
new file mode 100644
index 0000000..b5a9e52
--- /dev/null
+++ b/src/datepicker/index.ts
@@ -0,0 +1,2 @@
+export { DatePicker } from './datepicker';
+export type { DatePickerProps } from './datepicker';
diff --git a/src/divider/divider.tsx b/src/divider/divider.tsx
index c84659e..c124d1b 100644
--- a/src/divider/divider.tsx
+++ b/src/divider/divider.tsx
@@ -1,7 +1,11 @@
-import NativeDividerComponent, {
- type NativeProps,
-} from '../specs/NativeDividerComponent';
+// import DividerNativeComponent, {
+// type NativeProps,
+// } from '../specs/DividerNativeComponent';
-export default function Divider(props: NativeProps) {
- return ;
+export default function Divider() {
+ return null;
}
+
+// export default function Divider(props: NativeProps) {
+// return ;
+// }
diff --git a/src/divider/index.ts b/src/divider/index.ts
index e81774a..53f643c 100644
--- a/src/divider/index.ts
+++ b/src/divider/index.ts
@@ -1,2 +1,2 @@
export { default } from './divider';
-export type { NativeProps as DividerProps } from '../specs/NativeDividerComponent';
+export type { NativeProps as DividerProps } from '../specs/DividerNativeComponent';
diff --git a/src/index.tsx b/src/index.tsx
index 5efdfe2..6a83de4 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,2 +1,26 @@
export { default as Divider } from './divider';
export type { DividerProps } from './divider';
+
+export { DatePicker } from './datepicker';
+export type { DatePickerProps } from './datepicker';
+
+export { TimePicker } from './timepicker';
+export type { TimePickerProps } from './timepicker';
+
+export { Snackbar } from './snackbar';
+export type { SnackbarProps } from './snackbar';
+
+export { AlertDialog } from './alert_dialog';
+export type { AlertDialogProps } from './alert_dialog';
+
+export { OptionsDialog } from './options_dialog';
+export type { OptionsDialogProps } from './options_dialog';
+
+export { RangePicker } from './rangepicker';
+export type { RangePickerProps } from './rangepicker';
+
+export { Menu } from './menu';
+export type { MenuProps } from './menu';
+
+export { Colors } from './colors';
+export type { MaterialColors, MaterialThemes } from './colors';
diff --git a/src/menu/index.ts b/src/menu/index.ts
new file mode 100644
index 0000000..f5802f6
--- /dev/null
+++ b/src/menu/index.ts
@@ -0,0 +1,2 @@
+export { Menu } from './menu';
+export type { MenuProps } from './menu';
diff --git a/src/menu/menu.ios.ts b/src/menu/menu.ios.ts
new file mode 100644
index 0000000..01bd3e7
--- /dev/null
+++ b/src/menu/menu.ios.ts
@@ -0,0 +1,3 @@
+export const Menu = function Menu() {
+ return null;
+};
diff --git a/src/menu/menu.ts b/src/menu/menu.ts
new file mode 100644
index 0000000..524256b
--- /dev/null
+++ b/src/menu/menu.ts
@@ -0,0 +1,167 @@
+import type { ReactElement, ReactNode } from 'react';
+import NativeMenu from '../specs/NativeMenu';
+import React, { useRef } from 'react';
+import { findNodeHandle } from 'react-native';
+
+export type Gravity =
+ | 'top'
+ | 'bottom'
+ | 'left'
+ | 'right'
+ | 'center'
+ | 'center_vertical'
+ | 'center_horizontal'
+ | 'fill'
+ | 'fill_vertical'
+ | 'fill_horizontal'
+ | 'start'
+ | 'end'
+ | 'clip_vertical'
+ | 'clip_horizontal'
+ | 'no_gravity';
+
+function show(
+ props: { id: number; gravity?: Gravity },
+ items: Array,
+ callbacks: { [key: string]: () => void }
+) {
+ NativeMenu.show(props, items, (selectedId: number) => {
+ callbacks[selectedId]?.();
+ });
+}
+
+export interface MenuProps {
+ children: ReactNode;
+ gravity?: Gravity;
+}
+
+export function Menu({ children, gravity }: MenuProps) {
+ let trigger: ReactElement | null = null;
+ let items: ReactNode | null = null;
+ const pressableRef = useRef();
+
+ React.Children.forEach(children, (child) => {
+ if (React.isValidElement(child)) {
+ if (child.type === Menu.Trigger) {
+ trigger = child;
+ } else if (child.type === Menu.Items) {
+ items = child;
+ }
+ }
+ });
+
+ if (!trigger) return children;
+ const pressable = React.Children.toArray(
+ (trigger as ReactElement).props?.children
+ )?.[0];
+
+ if (!React.isValidElement(pressable)) return children;
+
+ function handleShow(e: any) {
+ const idCounter = { current: 1 };
+ const callbacks = {};
+
+ if (!pressableRef.current) return console.warn('No ref found for trigger');
+
+ const nativeId = findNodeHandle(pressableRef.current);
+ if (!nativeId) return console.warn('No native id found for trigger');
+
+ const itemsJSON = convertItemsToJSON(
+ (items as ReactElement)?.props?.children,
+ idCounter,
+ callbacks
+ );
+
+ if (!itemsJSON) return console.warn('No items found for menu');
+ show({ id: nativeId, gravity }, itemsJSON, callbacks);
+
+ if (!pressable) return console.warn('No component found for trigger');
+ (pressable as ReactElement).props?.onPress?.(e);
+ }
+
+ const pressableClone = React.cloneElement(pressable, {
+ ref: pressableRef,
+ onPress: handleShow,
+ });
+
+ return pressableClone;
+}
+
+interface MenuTriggerProps {
+ children: ReactElement;
+}
+Menu.Trigger = function Trigger({ children }: MenuTriggerProps) {
+ return children;
+};
+
+interface ItemsProps {
+ children: ReactNode;
+}
+
+Menu.Items = function Items(_props: ItemsProps) {
+ return null;
+};
+
+interface MenuItemProps {
+ title: string;
+ groupId?: number;
+ onSelect: () => void;
+ isCheckable?: boolean;
+ isChecked?: boolean;
+ icon?: string;
+}
+
+Menu.Item = function Item(_props: MenuItemProps) {
+ return null;
+};
+
+interface MenuSubMenuProps {
+ children: ReactNode;
+ title: string;
+ groupId?: number;
+ icon?: string;
+}
+
+Menu.SubMenu = function SubMenu(_props: MenuSubMenuProps) {
+ return null;
+};
+
+function convertItemsToJSON(
+ items: ReactNode | null,
+ idCounter: { current: number },
+ callbacks: { [key: string]: () => void }
+) {
+ if (!items) {
+ return null;
+ }
+
+ const itemsJSON: Array = [];
+
+ React.Children.forEach(items, (child) => {
+ if (React.isValidElement(child)) {
+ if (child.type === Menu.Item) {
+ const itemId = idCounter.current++;
+ callbacks[itemId] = child.props?.onSelect;
+ itemsJSON.push({
+ itemId,
+ title: child.props.title,
+ groupId: child.props.groupId,
+ isChecked: child.props.isChecked,
+ isCheckable: child.props.isCheckable,
+ icon: child.props.icon,
+ type: 'item',
+ });
+ } else if (child.type === Menu.SubMenu) {
+ itemsJSON.push({
+ title: child.props.title,
+ groupId: child.props.groupId,
+ icon: child.props.icon,
+ items: convertItemsToJSON(child.props.children, idCounter, callbacks),
+ type: 'submenu',
+ });
+ }
+ }
+ });
+
+ return itemsJSON;
+}
diff --git a/src/options_dialog/index.ts b/src/options_dialog/index.ts
new file mode 100644
index 0000000..55b7c4f
--- /dev/null
+++ b/src/options_dialog/index.ts
@@ -0,0 +1,2 @@
+export { OptionsDialog } from './options_dialog';
+export type { OptionsDialogProps } from './options_dialog';
diff --git a/src/options_dialog/options_dialog.ios.ts b/src/options_dialog/options_dialog.ios.ts
new file mode 100644
index 0000000..0c254dd
--- /dev/null
+++ b/src/options_dialog/options_dialog.ios.ts
@@ -0,0 +1,3 @@
+export const OptionsDialog = {
+ show: () => {},
+};
diff --git a/src/options_dialog/options_dialog.ts b/src/options_dialog/options_dialog.ts
new file mode 100644
index 0000000..93c48df
--- /dev/null
+++ b/src/options_dialog/options_dialog.ts
@@ -0,0 +1,52 @@
+import NativeOptionsDialog, {
+ type ShowProps,
+} from '../specs/NativeOptionsDialog';
+
+function show({
+ onNegativePress,
+ onPositivePress,
+ onNeutralPress,
+ onCancel,
+ ...props
+}: OptionsDialogProps) {
+ validateSelections(props);
+
+ NativeOptionsDialog.show(
+ props,
+ onPositivePress,
+ onNegativePress,
+ onNeutralPress,
+ onCancel
+ );
+}
+
+function validateSelections({ pickerType, selected }: OptionsDialogProps) {
+ const length = selected?.length || 0;
+
+ if (pickerType === 'singlechoice' && length > 1)
+ console.warn(
+ `Single choice picker can only have one selected item. Provided selected: ${selected}`
+ );
+
+ const isRowPicker = pickerType === 'row' || pickerType === undefined;
+ if (isRowPicker && length > 0)
+ console.warn(
+ `Row picker can't have selections. Provided selected: ${selected}`
+ );
+}
+
+export const OptionsDialog = {
+ show,
+};
+
+export type OptionsDialogProps = Omit<
+ ShowProps,
+ 'headerAlignment' | 'pickerType'
+> & {
+ onPositivePress?: (selectedIndexes?: number[]) => void;
+ onNegativePress?: (selectedIndexes?: number[]) => void;
+ onNeutralPress?: (selectedIndexes?: number[]) => void;
+ onCancel?: (selectedIndexes?: number[]) => void;
+ headerAlignment?: 'start' | 'center';
+ pickerType?: 'row' | 'singlechoice' | 'multiselect';
+};
diff --git a/src/rangepicker/index.ts b/src/rangepicker/index.ts
new file mode 100644
index 0000000..ad48a87
--- /dev/null
+++ b/src/rangepicker/index.ts
@@ -0,0 +1,2 @@
+export { RangePicker } from './rangepicker';
+export type { RangePickerProps } from './rangepicker';
diff --git a/src/rangepicker/rangepicker.ios.ts b/src/rangepicker/rangepicker.ios.ts
new file mode 100644
index 0000000..dfc5331
--- /dev/null
+++ b/src/rangepicker/rangepicker.ios.ts
@@ -0,0 +1,3 @@
+export const RangePicker = {
+ show: () => {},
+};
diff --git a/src/rangepicker/rangepicker.ts b/src/rangepicker/rangepicker.ts
new file mode 100644
index 0000000..78b8570
--- /dev/null
+++ b/src/rangepicker/rangepicker.ts
@@ -0,0 +1,50 @@
+import NativeRangePicker, { type ShowProps } from '../specs/NativeRangePicker';
+
+export type Range = {
+ start: Date;
+ end: Date;
+};
+
+function show({
+ onChange,
+ onCancel,
+ value,
+ maxDate,
+ minDate,
+ ...props
+}: RangePickerProps): Promise {
+ function handleChange(startTimestamp: number, endTimestamp: number) {
+ onChange({ start: new Date(startTimestamp), end: new Date(endTimestamp) });
+ }
+
+ return NativeRangePicker.show(
+ {
+ start: value?.start?.getTime(),
+ end: value?.end?.getTime(),
+ maxDate: maxDate?.getTime(),
+ minDate: minDate?.getTime(),
+ ...props,
+ },
+ handleChange,
+ onCancel
+ );
+}
+
+export const RangePicker = {
+ show,
+};
+
+export type RangePickerProps = Omit<
+ ShowProps,
+ 'value' | 'inputMode' | 'maxDate' | 'minDate' | 'start' | 'end'
+> & {
+ onChange: (newValue: Range) => void;
+ onCancel?: () => void;
+ value?: {
+ start?: Date;
+ end?: Date;
+ };
+ maxDate?: Date;
+ minDate?: Date;
+ inputMode?: 'text' | 'calendar';
+};
diff --git a/src/snackbar/index.ts b/src/snackbar/index.ts
new file mode 100644
index 0000000..1766af2
--- /dev/null
+++ b/src/snackbar/index.ts
@@ -0,0 +1,2 @@
+export { Snackbar } from './snackbar';
+export type { SnackbarProps } from './snackbar';
diff --git a/src/snackbar/snackbar.ios.ts b/src/snackbar/snackbar.ios.ts
new file mode 100644
index 0000000..c6ab8ad
--- /dev/null
+++ b/src/snackbar/snackbar.ios.ts
@@ -0,0 +1,3 @@
+export const DatePicker = {
+ show: () => {},
+};
diff --git a/src/snackbar/snackbar.ts b/src/snackbar/snackbar.ts
new file mode 100644
index 0000000..dcd4a87
--- /dev/null
+++ b/src/snackbar/snackbar.ts
@@ -0,0 +1,29 @@
+import NativeSnackbar, { type ShowProps } from '../specs/NativeSnackbar';
+
+function show({ action, ...props }: SnackbarProps) {
+ return NativeSnackbar.show(
+ {
+ actionText: action?.text,
+ actionTextColor: action?.textColor,
+ ...props,
+ },
+ action?.onPress
+ );
+}
+
+export const Snackbar = {
+ show,
+};
+
+export type SnackbarProps = Omit<
+ ShowProps,
+ 'duration' | 'animationMode' | 'actionText' | 'actionTextColor'
+> & {
+ duration?: 'short' | 'long' | 'indefinite';
+ animationMode?: 'slide' | 'fade';
+ action?: {
+ text: string;
+ onPress?: () => void;
+ textColor?: string;
+ };
+};
diff --git a/src/specs/NativeAlertDialog.ts b/src/specs/NativeAlertDialog.ts
new file mode 100644
index 0000000..601710d
--- /dev/null
+++ b/src/specs/NativeAlertDialog.ts
@@ -0,0 +1,25 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface ShowProps {
+ title: string;
+ message?: string;
+ negativeButtonText?: string;
+ positiveButtonText?: string;
+ neutralButtonText?: string;
+ cancelable?: boolean;
+ headerAlignment?: string;
+ icon?: string;
+}
+
+export interface Spec extends TurboModule {
+ show: (
+ props: ShowProps,
+ onPositivePress?: () => void,
+ onNegativePress?: () => void,
+ onNeutralPress?: () => void,
+ onCancel?: () => void
+ ) => void;
+}
+
+export default TurboModuleRegistry.getEnforcing('RTNAlertDialog');
diff --git a/src/specs/NativeColors.ts b/src/specs/NativeColors.ts
new file mode 100644
index 0000000..c0f4bd2
--- /dev/null
+++ b/src/specs/NativeColors.ts
@@ -0,0 +1,63 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface MaterialThemes {
+ light: MaterialColors;
+ dark: MaterialColors;
+}
+
+export interface MaterialColors {
+ errorContainer: string;
+ onBackground: string;
+ onError: string;
+ onErrorContainer: string;
+ onPrimary: string;
+ onPrimaryContainer: string;
+ onPrimaryFixed: string;
+ onPrimaryFixedVariant: string;
+ onPrimarySurface: string;
+ onSecondary: string;
+ onSecondaryContainer: string;
+ onSecondaryFixed: string;
+ onSecondaryFixedVariant: string;
+ onSurface: string;
+ onSurfaceInverse: string;
+ onSurfaceVariant: string;
+ onTertiary: string;
+ onTertiaryContainer: string;
+ onTertiaryFixed: string;
+ onTertiaryFixedVariant: string;
+ outline: string;
+ outlineVariant: string;
+ primaryContainer: string;
+ primaryFixed: string;
+ primaryFixedDim: string;
+ primaryInverse: string;
+ primarySurface: string;
+ primaryVariant: string;
+ secondary: string;
+ secondaryContainer: string;
+ secondaryFixed: string;
+ secondaryFixedDim: string;
+ secondaryVariant: string;
+ surface: string;
+ surfaceBright: string;
+ surfaceContainer: string;
+ surfaceContainerHigh: string;
+ surfaceContainerHighest: string;
+ surfaceContainerLow: string;
+ surfaceContainerLowest: string;
+ surfaceDim: string;
+ surfaceInverse: string;
+ surfaceVariant: string;
+ tertiary: string;
+ tertiaryContainer: string;
+ tertiaryFixed: string;
+ tertiaryFixedDim: string;
+}
+
+export interface Spec extends TurboModule {
+ getColors: () => MaterialThemes;
+}
+
+export default TurboModuleRegistry.getEnforcing('RTNColors');
diff --git a/src/specs/NativeDatePicker.ts b/src/specs/NativeDatePicker.ts
new file mode 100644
index 0000000..09eed02
--- /dev/null
+++ b/src/specs/NativeDatePicker.ts
@@ -0,0 +1,24 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface ShowProps {
+ inputMode?: string;
+ fullscreen?: boolean;
+ value?: number;
+ positiveButtonText?: string;
+ negativeButtonText?: string;
+ title?: string;
+ maxDate?: number;
+ minDate?: number;
+ firstDayOfWeek?: number;
+}
+
+export interface Spec extends TurboModule {
+ show: (
+ props: ShowProps,
+ onChange: (newValue: number) => void,
+ onCancel?: () => void
+ ) => Promise;
+}
+
+export default TurboModuleRegistry.getEnforcing('RTNDatePicker');
diff --git a/src/specs/NativeDividerComponent.ts b/src/specs/NativeDividerComponent.ts
deleted file mode 100644
index 3ebee01..0000000
--- a/src/specs/NativeDividerComponent.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
-import type { ViewProps } from 'react-native';
-
-export interface NativeProps extends ViewProps {
- dividerColor?: string;
-}
-
-export default codegenNativeComponent('RTNDivider');
diff --git a/src/specs/NativeMenu.ts b/src/specs/NativeMenu.ts
new file mode 100644
index 0000000..512fc8a
--- /dev/null
+++ b/src/specs/NativeMenu.ts
@@ -0,0 +1,25 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface ShowProps {
+ id: number;
+ gravity?: string;
+}
+
+interface Item {
+ title?: string;
+ groupId?: number;
+ icon?: string;
+ isCheckable?: boolean;
+ isChecked?: boolean;
+}
+
+export interface Spec extends TurboModule {
+ show: (
+ props: ShowProps,
+ items: Array- ,
+ onSelect: (id: number) => void
+ ) => void;
+}
+
+export default TurboModuleRegistry.getEnforcing('RTNMenu');
diff --git a/src/specs/NativeOptionsDialog.ts b/src/specs/NativeOptionsDialog.ts
new file mode 100644
index 0000000..2b8151c
--- /dev/null
+++ b/src/specs/NativeOptionsDialog.ts
@@ -0,0 +1,27 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface ShowProps {
+ title: string;
+ negativeButtonText?: string;
+ positiveButtonText?: string;
+ neutralButtonText?: string;
+ cancelable?: boolean;
+ headerAlignment?: string;
+ pickerType?: string;
+ options: string[];
+ selected?: number[];
+ icon?: string;
+}
+
+export interface Spec extends TurboModule {
+ show: (
+ props: ShowProps,
+ onPositivePress?: () => void,
+ onNegativePress?: () => void,
+ onNeutralPress?: () => void,
+ onCancel?: () => void
+ ) => void;
+}
+
+export default TurboModuleRegistry.getEnforcing('RTNOptionsDialog');
diff --git a/src/specs/NativeRangePicker.ts b/src/specs/NativeRangePicker.ts
new file mode 100644
index 0000000..64d6acc
--- /dev/null
+++ b/src/specs/NativeRangePicker.ts
@@ -0,0 +1,25 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface ShowProps {
+ inputMode?: string;
+ fullscreen?: boolean;
+ start?: number;
+ end?: number;
+ positiveButtonText?: string;
+ negativeButtonText?: string;
+ title?: string;
+ maxDate?: number;
+ minDate?: number;
+ firstDayOfWeek?: number;
+}
+
+export interface Spec extends TurboModule {
+ show: (
+ props: ShowProps,
+ onChange: (startTimestamp: number, endTimestamp: number) => void,
+ onCancel?: () => void
+ ) => Promise;
+}
+
+export default TurboModuleRegistry.getEnforcing('RTNRangePicker');
diff --git a/src/specs/NativeSnackbar.ts b/src/specs/NativeSnackbar.ts
new file mode 100644
index 0000000..45277fc
--- /dev/null
+++ b/src/specs/NativeSnackbar.ts
@@ -0,0 +1,19 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface ShowProps {
+ duration?: string;
+ animationMode?: string;
+ textColor?: string;
+ text: string;
+ textMaxLines?: number;
+ actionText?: string;
+ actionTextColor?: string;
+ backgroundColor?: string;
+}
+
+export interface Spec extends TurboModule {
+ show: (props: ShowProps, onActionPress?: () => void) => void;
+}
+
+export default TurboModuleRegistry.getEnforcing('RTNSnackbar');
diff --git a/src/specs/NativeTimePicker.ts b/src/specs/NativeTimePicker.ts
new file mode 100644
index 0000000..08c1951
--- /dev/null
+++ b/src/specs/NativeTimePicker.ts
@@ -0,0 +1,22 @@
+import type { TurboModule } from 'react-native';
+import { TurboModuleRegistry } from 'react-native';
+
+export interface ShowProps {
+ inputMode?: string;
+ positiveButtonText?: string;
+ negativeButtonText?: string;
+ title?: string;
+ is24HourFormat?: boolean;
+ hour?: number;
+ minute?: number;
+}
+
+export interface Spec extends TurboModule {
+ show: (
+ props: ShowProps,
+ onChange: (result: { hour: number; minute: number }) => void,
+ onCancel?: () => void
+ ) => Promise;
+}
+
+export default TurboModuleRegistry.getEnforcing('RTNTimePicker');
diff --git a/src/timepicker/index.ts b/src/timepicker/index.ts
new file mode 100644
index 0000000..74da422
--- /dev/null
+++ b/src/timepicker/index.ts
@@ -0,0 +1,2 @@
+export { TimePicker } from './timepicker';
+export type { TimePickerProps } from './timepicker';
diff --git a/src/timepicker/timepicker.ios.ts b/src/timepicker/timepicker.ios.ts
new file mode 100644
index 0000000..0338def
--- /dev/null
+++ b/src/timepicker/timepicker.ios.ts
@@ -0,0 +1,3 @@
+export const TimePicker = {
+ show: () => {},
+};
diff --git a/src/timepicker/timepicker.ts b/src/timepicker/timepicker.ts
new file mode 100644
index 0000000..a07e39c
--- /dev/null
+++ b/src/timepicker/timepicker.ts
@@ -0,0 +1,25 @@
+import NativeTimePicker, { type ShowProps } from '../specs/NativeTimePicker';
+
+function show({
+ onChange,
+ onCancel,
+ ...props
+}: TimePickerProps): Promise {
+ return NativeTimePicker.show(
+ {
+ ...props,
+ },
+ onChange,
+ onCancel
+ );
+}
+
+export const TimePicker = {
+ show,
+};
+
+export type TimePickerProps = Omit & {
+ onChange: (result: { hour: number; minute: number }) => void;
+ onCancel?: () => void;
+ inputMode?: 'keyboard' | 'clock';
+};
diff --git a/yarn.lock b/yarn.lock
index dc8f4fb..09517b0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2957,8 +2957,8 @@ __metadata:
linkType: hard
"@release-it/conventional-changelog@npm:^9.0.2":
- version: 9.0.3
- resolution: "@release-it/conventional-changelog@npm:9.0.3"
+ version: 9.0.4
+ resolution: "@release-it/conventional-changelog@npm:9.0.4"
dependencies:
concat-stream: ^2.0.0
conventional-changelog: ^6.0.0
@@ -2967,7 +2967,7 @@ __metadata:
semver: ^7.6.3
peerDependencies:
release-it: ^17.0.0
- checksum: 719df7cf906372070c0a927d8da5145f9b99f43da06699a4dd0a43867359959ea79fb0fac5e706bbcb6d42782d74bdc1f958eb3e6142cbf583a2c7efa3e20a18
+ checksum: fbe17cc1d83abd616fa1b02b3c52d964ba6bdc7e5d91984f5bd1aebcdde390b9d7d0e8e3d916dce64f658058429f65a92196d1c978018bb35973e15419272e65
languageName: node
linkType: hard
@@ -3448,7 +3448,7 @@ __metadata:
languageName: node
linkType: hard
-"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.2":
+"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2":
version: 7.1.3
resolution: "agent-base@npm:7.1.3"
checksum: 87bb7ee54f5ecf0ccbfcba0b07473885c43ecd76cb29a8db17d6137a19d9f9cd443a2a7c5fd8a3f24d58ad8145f9eb49116344a66b107e1aeab82cf2383f4753
@@ -4244,10 +4244,10 @@ __metadata:
languageName: node
linkType: hard
-"chalk@npm:5.3.0":
- version: 5.3.0
- resolution: "chalk@npm:5.3.0"
- checksum: 623922e077b7d1e9dedaea6f8b9e9352921f8ae3afe739132e0e00c275971bdd331268183b2628cf4ab1727c45ea1f28d7e24ac23ce1db1eb653c414ca8a5a80
+"chalk@npm:5.4.1, chalk@npm:^5.3.0":
+ version: 5.4.1
+ resolution: "chalk@npm:5.4.1"
+ checksum: 0c656f30b782fed4d99198825c0860158901f449a6b12b818b0aabad27ec970389e7e8767d0e00762175b23620c812e70c4fd92c0210e55fc2d993638b74e86e
languageName: node
linkType: hard
@@ -4261,13 +4261,6 @@ __metadata:
languageName: node
linkType: hard
-"chalk@npm:^5.3.0":
- version: 5.4.1
- resolution: "chalk@npm:5.4.1"
- checksum: 0c656f30b782fed4d99198825c0860158901f449a6b12b818b0aabad27ec970389e7e8767d0e00762175b23620c812e70c4fd92c0210e55fc2d993638b74e86e
- languageName: node
- linkType: hard
-
"char-regex@npm:^1.0.2":
version: 1.0.2
resolution: "char-regex@npm:1.0.2"
@@ -4331,7 +4324,7 @@ __metadata:
languageName: node
linkType: hard
-"ci-info@npm:^4.0.0":
+"ci-info@npm:^4.1.0":
version: 4.1.0
resolution: "ci-info@npm:4.1.0"
checksum: dcf286abdc1bb1c4218b91e4a617b49781b282282089b7188e1417397ea00c6b967848e2360fb9a6b10021bf18a627f20ef698f47c2c9c875aeffd1d2ea51d1e
@@ -6089,11 +6082,11 @@ __metadata:
linkType: hard
"fastq@npm:^1.6.0":
- version: 1.17.1
- resolution: "fastq@npm:1.17.1"
+ version: 1.18.0
+ resolution: "fastq@npm:1.18.0"
dependencies:
reusify: ^1.0.4
- checksum: a8c5b26788d5a1763f88bae56a8ddeee579f935a831c5fe7a8268cea5b0a91fbfe705f612209e02d639b881d7b48e461a50da4a10cfaa40da5ca7cc9da098d88
+ checksum: fb8d94318c2e5545a1913c1647b35e8b7825caaba888a98ef9887085e57f5a82104aefbb05f26c81d4e220f02b2ea6f2c999132186d8c77e6c681d91870191ba
languageName: node
linkType: hard
@@ -6876,7 +6869,7 @@ __metadata:
languageName: node
linkType: hard
-"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.3, https-proxy-agent@npm:^7.0.6":
+"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6":
version: 7.0.6
resolution: "https-proxy-agent@npm:7.0.6"
dependencies:
@@ -6940,13 +6933,13 @@ __metadata:
linkType: hard
"image-size@npm:^1.0.2":
- version: 1.1.1
- resolution: "image-size@npm:1.1.1"
+ version: 1.2.0
+ resolution: "image-size@npm:1.2.0"
dependencies:
queue: 6.0.2
bin:
image-size: bin/image-size.js
- checksum: 23b3a515dded89e7f967d52b885b430d6a5a903da954fce703130bfb6069d738d80e6588efd29acfaf5b6933424a56535aa7bf06867e4ebd0250c2ee51f19a4a
+ checksum: 6264ae22ea6f349480c5305f84cd1e64f9757442abf4baac79e29519cba38f7ccab90488996e5e4d0c232b2f44dc720576fdf3e7e63c161e49eb1d099e563f82
languageName: node
linkType: hard
@@ -9997,9 +9990,9 @@ __metadata:
languageName: node
linkType: hard
-"ora@npm:8.1.0":
- version: 8.1.0
- resolution: "ora@npm:8.1.0"
+"ora@npm:8.1.1":
+ version: 8.1.1
+ resolution: "ora@npm:8.1.1"
dependencies:
chalk: ^5.3.0
cli-cursor: ^5.0.0
@@ -10010,7 +10003,7 @@ __metadata:
stdin-discarder: ^0.2.2
string-width: ^7.2.0
strip-ansi: ^7.1.0
- checksum: 81b9a2627a687c2b16fa08b0ae0b3641b320bdbeca831eb323df0cbb1e5ddc096b94391ff342839a1db47f5a895cebb2a8d06c319a5d935fc48628f35a036107
+ checksum: 0cb79b9d8458ef0878e43d692fddb078c0885c82bbfa45e46de366f71fd506a75d8f9d5df71859624f7f0fe488c17d2e6882d7a35126214cf1a0e0c0f51248c4
languageName: node
linkType: hard
@@ -10125,7 +10118,7 @@ __metadata:
languageName: node
linkType: hard
-"pac-proxy-agent@npm:^7.0.1":
+"pac-proxy-agent@npm:^7.1.0":
version: 7.1.0
resolution: "pac-proxy-agent@npm:7.1.0"
dependencies:
@@ -10481,19 +10474,19 @@ __metadata:
languageName: node
linkType: hard
-"proxy-agent@npm:6.4.0":
- version: 6.4.0
- resolution: "proxy-agent@npm:6.4.0"
+"proxy-agent@npm:6.5.0":
+ version: 6.5.0
+ resolution: "proxy-agent@npm:6.5.0"
dependencies:
- agent-base: ^7.0.2
+ agent-base: ^7.1.2
debug: ^4.3.4
http-proxy-agent: ^7.0.1
- https-proxy-agent: ^7.0.3
+ https-proxy-agent: ^7.0.6
lru-cache: ^7.14.1
- pac-proxy-agent: ^7.0.1
+ pac-proxy-agent: ^7.1.0
proxy-from-env: ^1.1.0
- socks-proxy-agent: ^8.0.2
- checksum: 4d3794ad5e07486298902f0a7f250d0f869fa0e92d790767ca3f793a81374ce0ab6c605f8ab8e791c4d754da96656b48d1c24cb7094bfd310a15867e4a0841d7
+ socks-proxy-agent: ^8.0.5
+ checksum: d03ad2d171c2768280ade7ea6a7c5b1d0746215d70c0a16e02780c26e1d347edd27b3f48374661ae54ec0f7b41e6e45175b687baf333b36b1fd109a525154806
languageName: node
linkType: hard
@@ -10983,14 +10976,14 @@ __metadata:
linkType: hard
"release-it@npm:^17.10.0":
- version: 17.10.0
- resolution: "release-it@npm:17.10.0"
+ version: 17.11.0
+ resolution: "release-it@npm:17.11.0"
dependencies:
"@iarna/toml": 2.2.5
"@octokit/rest": 20.1.1
async-retry: 1.3.3
- chalk: 5.3.0
- ci-info: ^4.0.0
+ chalk: 5.4.1
+ ci-info: ^4.1.0
cosmiconfig: 9.0.0
execa: 8.0.0
git-url-parse: 14.0.0
@@ -11001,18 +10994,18 @@ __metadata:
mime-types: 2.1.35
new-github-release-url: 2.0.0
open: 10.1.0
- ora: 8.1.0
+ ora: 8.1.1
os-name: 5.1.0
- proxy-agent: 6.4.0
+ proxy-agent: 6.5.0
semver: 7.6.3
shelljs: 0.8.5
update-notifier: 7.3.1
url-join: 5.0.0
- wildcard-match: 5.1.3
+ wildcard-match: 5.1.4
yargs-parser: 21.1.1
bin:
release-it: bin/release-it.js
- checksum: 50d183c2cb09fa77e8087ceed15211182cb63d02ace1aa0a1289d402b7063abb9eaf117a7f5424de487545627e0a099c68f42450b8fc3de7855045aee3d668ed
+ checksum: 0f1e43e23e4144901af9ffbc9ad9b4ab689198ee268d4c8c6b33e798c35c208f7d133a8a57ce95c67e12db4c1e752913e4af92d3dc31bdb9467be808f7b04729
languageName: node
linkType: hard
@@ -11583,7 +11576,7 @@ __metadata:
languageName: node
linkType: hard
-"socks-proxy-agent@npm:^8.0.2, socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.5":
+"socks-proxy-agent@npm:^8.0.3, socks-proxy-agent@npm:^8.0.5":
version: 8.0.5
resolution: "socks-proxy-agent@npm:8.0.5"
dependencies:
@@ -12834,10 +12827,10 @@ __metadata:
languageName: node
linkType: hard
-"wildcard-match@npm:5.1.3":
- version: 5.1.3
- resolution: "wildcard-match@npm:5.1.3"
- checksum: a27d70b3f63be7f20054583de2210f4bd306101a93aa3bf0be99255a068ce95d51e7d92a1474282f913cad0d24e9f59949cd00cbe5134aa18f6d4289927d0e88
+"wildcard-match@npm:5.1.4":
+ version: 5.1.4
+ resolution: "wildcard-match@npm:5.1.4"
+ checksum: 96e8c13f26b7ae508c694ceb6721640707df55f22045870fbd3b7d8f58529d3616e8e59fb6992524db5e8b323c9fe7c3e92d92b5ae36707529d1f4f170c00e23
languageName: node
linkType: hard