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 ( + + + + + + console.log('Item 1 pressed')} + title="Bold" + groupId={1} + icon="baseline_access_alarm_24" + isCheckable={true} + /> + console.log('Item 2 pressed')} + title="Italic" + groupId={1} + icon="baseline_access_alarm_24" + isCheckable={true} + isChecked={true} + /> + console.log('Item 3 pressed')} + title="Delete" + groupId={2} + /> + + + console.log('Item 4 pressed')} + title="Restore default" + /> + + + + ); +} + +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