react-native-platform-components 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +259 -44
  2. package/android/src/main/java/com/platformcomponents/PCLiquidGlassView.kt +84 -0
  3. package/android/src/main/java/com/platformcomponents/PCLiquidGlassViewManager.kt +52 -0
  4. package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +241 -0
  5. package/android/src/main/java/com/platformcomponents/PCSegmentedControlViewManager.kt +105 -0
  6. package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +2 -0
  7. package/ios/PCDatePickerView.swift +16 -13
  8. package/ios/PCLiquidGlass.h +10 -0
  9. package/ios/PCLiquidGlass.mm +140 -0
  10. package/ios/PCLiquidGlass.swift +354 -0
  11. package/ios/PCSegmentedControl.h +10 -0
  12. package/ios/PCSegmentedControl.mm +194 -0
  13. package/ios/PCSegmentedControl.swift +200 -0
  14. package/ios/PCSelectionMenu.swift +1 -1
  15. package/lib/commonjs/LiquidGlass.js +72 -0
  16. package/lib/commonjs/LiquidGlass.js.map +1 -0
  17. package/lib/commonjs/LiquidGlassNativeComponent.ts +110 -0
  18. package/lib/commonjs/SegmentedControl.js +93 -0
  19. package/lib/commonjs/SegmentedControl.js.map +1 -0
  20. package/lib/commonjs/SegmentedControlNativeComponent.ts +79 -0
  21. package/lib/commonjs/index.js +22 -0
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/module/LiquidGlass.js +64 -0
  24. package/lib/module/LiquidGlass.js.map +1 -0
  25. package/lib/module/LiquidGlassNativeComponent.ts +110 -0
  26. package/lib/module/SegmentedControl.js +87 -0
  27. package/lib/module/SegmentedControl.js.map +1 -0
  28. package/lib/module/SegmentedControlNativeComponent.ts +79 -0
  29. package/lib/module/index.js +2 -0
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/typescript/commonjs/src/LiquidGlass.d.ts +96 -0
  32. package/lib/typescript/commonjs/src/LiquidGlass.d.ts.map +1 -0
  33. package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts +93 -0
  34. package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts.map +1 -0
  35. package/lib/typescript/commonjs/src/SegmentedControl.d.ts +62 -0
  36. package/lib/typescript/commonjs/src/SegmentedControl.d.ts.map +1 -0
  37. package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts +63 -0
  38. package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -0
  39. package/lib/typescript/commonjs/src/index.d.ts +2 -0
  40. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  41. package/lib/typescript/module/src/LiquidGlass.d.ts +96 -0
  42. package/lib/typescript/module/src/LiquidGlass.d.ts.map +1 -0
  43. package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts +93 -0
  44. package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts.map +1 -0
  45. package/lib/typescript/module/src/SegmentedControl.d.ts +62 -0
  46. package/lib/typescript/module/src/SegmentedControl.d.ts.map +1 -0
  47. package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts +63 -0
  48. package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -0
  49. package/lib/typescript/module/src/index.d.ts +2 -0
  50. package/lib/typescript/module/src/index.d.ts.map +1 -1
  51. package/package.json +13 -4
  52. package/react-native.config.js +1 -0
  53. package/shared/PCSegmentedControlComponentDescriptors-custom.h +22 -0
  54. package/shared/PCSegmentedControlShadowNode-custom.cpp +54 -0
  55. package/shared/PCSegmentedControlShadowNode-custom.h +56 -0
  56. package/shared/PCSegmentedControlState-custom.h +62 -0
  57. package/shared/react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h +1 -0
  58. package/src/LiquidGlass.tsx +169 -0
  59. package/src/LiquidGlassNativeComponent.ts +110 -0
  60. package/src/SegmentedControl.tsx +178 -0
  61. package/src/SegmentedControlNativeComponent.ts +79 -0
  62. package/src/index.tsx +2 -0
@@ -0,0 +1,241 @@
1
+ package com.platformcomponents
2
+
3
+ import android.content.Context
4
+ import android.text.TextUtils
5
+ import android.view.View
6
+ import android.widget.FrameLayout
7
+ import com.facebook.react.bridge.WritableNativeMap
8
+ import com.facebook.react.uimanager.PixelUtil
9
+ import com.facebook.react.uimanager.StateWrapper
10
+ import com.facebook.react.views.scroll.ReactScrollViewHelper
11
+ import com.google.android.material.button.MaterialButton
12
+ import com.google.android.material.button.MaterialButtonToggleGroup
13
+
14
+ class PCSegmentedControlView(context: Context) : FrameLayout(context), ReactScrollViewHelper.HasStateWrapper {
15
+
16
+ data class Segment(
17
+ val label: String,
18
+ val value: String,
19
+ val disabled: Boolean,
20
+ val icon: String
21
+ )
22
+
23
+ companion object {
24
+ private const val TAG = "PCSegmentedControl"
25
+ }
26
+
27
+ // --- State Wrapper for Fabric state updates ---
28
+ override var stateWrapper: StateWrapper? = null
29
+
30
+ private var lastReportedWidth: Float = 0f
31
+ private var lastReportedHeight: Float = 0f
32
+
33
+ // --- Props ---
34
+ var segments: List<Segment> = emptyList()
35
+ var selectedValue: String = "" // sentinel for none
36
+ var interactivity: String = "enabled" // "enabled" | "disabled"
37
+ var selectionRequired: Boolean = false
38
+
39
+ // --- Events ---
40
+ var onSelect: ((index: Int, value: String) -> Unit)? = null
41
+
42
+ // --- UI ---
43
+ private var toggleGroup: MaterialButtonToggleGroup? = null
44
+ private val buttonIdToSegment: MutableMap<Int, Segment> = mutableMapOf()
45
+ private var suppressCallbacks = false
46
+
47
+ init {
48
+ minimumHeight = (PCConstants.MIN_TOUCH_TARGET_HEIGHT_DP * resources.displayMetrics.density).toInt()
49
+ rebuildUI()
50
+ }
51
+
52
+ // ---- Public apply* (called by manager) ----
53
+
54
+ fun applySegments(newSegments: List<Segment>) {
55
+ if (segments == newSegments) return
56
+ segments = newSegments
57
+ rebuildUI()
58
+ }
59
+
60
+ fun applySelectedValue(value: String) {
61
+ if (selectedValue == value) return
62
+ selectedValue = value
63
+ updateSelection()
64
+ }
65
+
66
+ fun applyInteractivity(value: String?) {
67
+ val newValue = if (value == "disabled") "disabled" else "enabled"
68
+ if (interactivity == newValue) return
69
+ interactivity = newValue
70
+ updateEnabled()
71
+ }
72
+
73
+ fun applyAndroidProps(required: Boolean) {
74
+ if (selectionRequired != required) {
75
+ selectionRequired = required
76
+ rebuildUI()
77
+ }
78
+ }
79
+
80
+ // ---- UI Building ----
81
+
82
+ private fun rebuildUI() {
83
+ removeAllViews()
84
+ buttonIdToSegment.clear()
85
+
86
+ val group = MaterialButtonToggleGroup(context).apply {
87
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
88
+ isSingleSelection = true
89
+ isSelectionRequired = selectionRequired
90
+ }
91
+
92
+ // Calculate if we need compact mode (many segments or long labels)
93
+ val totalLabelLength = segments.sumOf { it.label.length }
94
+ val useCompactMode = segments.size > 3 || totalLabelLength > 20
95
+
96
+ for ((index, segment) in segments.withIndex()) {
97
+ val button = MaterialButton(context, null, com.google.android.material.R.attr.materialButtonOutlinedStyle).apply {
98
+ id = View.generateViewId()
99
+ text = segment.label
100
+ isAllCaps = false // Preserve original text casing
101
+ contentDescription = segment.label // For accessibility and Detox matching
102
+ isEnabled = !segment.disabled && interactivity == "enabled"
103
+
104
+ // Enable text truncation with ellipsis when space is limited
105
+ ellipsize = TextUtils.TruncateAt.END
106
+ maxLines = 1
107
+
108
+ // Reduce horizontal padding in compact mode to fit more content
109
+ if (useCompactMode) {
110
+ val compactPadding = (8 * resources.displayMetrics.density).toInt()
111
+ setPaddingRelative(compactPadding, paddingTop, compactPadding, paddingBottom)
112
+ iconPadding = (4 * resources.displayMetrics.density).toInt()
113
+ }
114
+
115
+ // Set icon if available
116
+ if (segment.icon.isNotEmpty()) {
117
+ val resId = context.resources.getIdentifier(
118
+ segment.icon, "drawable", context.packageName
119
+ )
120
+ if (resId != 0) {
121
+ setIconResource(resId)
122
+ }
123
+ }
124
+
125
+ // Handle click to trigger selection (needed for Detox taps)
126
+ setOnClickListener {
127
+ if (!suppressCallbacks && isEnabled) {
128
+ group.check(id)
129
+ }
130
+ }
131
+ }
132
+
133
+ buttonIdToSegment[button.id] = segment
134
+ group.addView(button)
135
+ }
136
+
137
+ group.addOnButtonCheckedListener { _, checkedId, isChecked ->
138
+ if (suppressCallbacks) return@addOnButtonCheckedListener
139
+ if (!isChecked) return@addOnButtonCheckedListener
140
+
141
+ val segment = buttonIdToSegment[checkedId] ?: return@addOnButtonCheckedListener
142
+ val index = segments.indexOf(segment)
143
+ if (index >= 0) {
144
+ onSelect?.invoke(index, segment.value)
145
+ }
146
+ }
147
+
148
+ addView(group)
149
+ toggleGroup = group
150
+
151
+ updateSelection()
152
+ updateEnabled()
153
+ requestLayout()
154
+ }
155
+
156
+ private fun updateSelection() {
157
+ suppressCallbacks = true
158
+ val group = toggleGroup ?: return
159
+
160
+ if (selectedValue.isEmpty()) {
161
+ group.clearChecked()
162
+ } else {
163
+ for ((id, segment) in buttonIdToSegment) {
164
+ if (segment.value == selectedValue) {
165
+ group.check(id)
166
+ break
167
+ }
168
+ }
169
+ }
170
+ suppressCallbacks = false
171
+ }
172
+
173
+ private fun updateEnabled() {
174
+ val enabled = interactivity == "enabled"
175
+ alpha = if (enabled) 1f else 0.5f
176
+
177
+ val group = toggleGroup ?: return
178
+ for (i in 0 until group.childCount) {
179
+ val button = group.getChildAt(i) as? MaterialButton ?: continue
180
+ val segment = buttonIdToSegment[button.id] ?: continue
181
+ button.isEnabled = enabled && !segment.disabled
182
+ }
183
+ }
184
+
185
+ // ---- Measurement ----
186
+
187
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
188
+ // Measure children with UNSPECIFIED height to get intrinsic size
189
+ val unconstrainedHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
190
+ super.onMeasure(widthMeasureSpec, unconstrainedHeightSpec)
191
+
192
+ // Get the intrinsic height from children
193
+ val childHeight = if (toggleGroup != null) {
194
+ toggleGroup!!.measuredHeight
195
+ } else {
196
+ 0
197
+ }
198
+
199
+ // Use the maximum of child height and minimum touch target
200
+ val intrinsicHeight = childHeight.coerceAtLeast(minimumHeight)
201
+
202
+ // IMPORTANT: Always use intrinsic height regardless of Yoga constraints.
203
+ // Fabric may give us 0 height initially before state is updated.
204
+ // The state update from onLayout will trigger proper re-layout.
205
+ setMeasuredDimension(measuredWidth, intrinsicHeight)
206
+ }
207
+
208
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
209
+ super.onLayout(changed, left, top, right, bottom)
210
+ updateFrameSizeState()
211
+ }
212
+
213
+ /**
214
+ * Update Fabric state with the measured frame size.
215
+ * This allows the shadow node to use actual measured dimensions for Yoga layout.
216
+ */
217
+ private fun updateFrameSizeState() {
218
+ val wrapper = stateWrapper ?: return
219
+ val group = toggleGroup ?: return
220
+
221
+ // Measure the toggle group with exact width and unspecified height
222
+ val widthSpec = MeasureSpec.makeMeasureSpec(width.coerceAtLeast(1), MeasureSpec.EXACTLY)
223
+ val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
224
+ group.measure(widthSpec, heightSpec)
225
+
226
+ val widthDp = PixelUtil.toDIPFromPixel(width.toFloat())
227
+ val heightDp = PixelUtil.toDIPFromPixel(group.measuredHeight.toFloat())
228
+
229
+ // Only update if changed
230
+ if (widthDp != lastReportedWidth || heightDp != lastReportedHeight) {
231
+ lastReportedWidth = widthDp
232
+ lastReportedHeight = heightDp
233
+
234
+ val stateData = WritableNativeMap().apply {
235
+ putDouble("width", widthDp.toDouble())
236
+ putDouble("height", heightDp.toDouble())
237
+ }
238
+ wrapper.updateState(stateData)
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,105 @@
1
+ package com.platformcomponents
2
+
3
+ import com.facebook.react.bridge.ReadableArray
4
+ import com.facebook.react.bridge.ReadableMap
5
+ import com.facebook.react.uimanager.ReactStylesDiffMap
6
+ import com.facebook.react.uimanager.SimpleViewManager
7
+ import com.facebook.react.uimanager.StateWrapper
8
+ import com.facebook.react.uimanager.ThemedReactContext
9
+ import com.facebook.react.uimanager.ViewManagerDelegate
10
+ import com.facebook.react.uimanager.UIManagerHelper
11
+ import com.facebook.react.uimanager.events.Event
12
+ import com.facebook.react.uimanager.events.RCTEventEmitter
13
+ import com.facebook.react.viewmanagers.PCSegmentedControlManagerDelegate
14
+ import com.facebook.react.viewmanagers.PCSegmentedControlManagerInterface
15
+
16
+ class PCSegmentedControlViewManager :
17
+ SimpleViewManager<PCSegmentedControlView>(),
18
+ PCSegmentedControlManagerInterface<PCSegmentedControlView> {
19
+
20
+ companion object {
21
+ private const val TAG = "PCSegmentedControl"
22
+ }
23
+
24
+ private val delegate: ViewManagerDelegate<PCSegmentedControlView> =
25
+ PCSegmentedControlManagerDelegate(this)
26
+
27
+ override fun getName(): String = "PCSegmentedControl"
28
+
29
+ override fun getDelegate(): ViewManagerDelegate<PCSegmentedControlView> = delegate
30
+
31
+ override fun createViewInstance(reactContext: ThemedReactContext): PCSegmentedControlView {
32
+ return PCSegmentedControlView(reactContext)
33
+ }
34
+
35
+ /**
36
+ * Pass the StateWrapper to the view so it can update Fabric state with measured dimensions.
37
+ */
38
+ override fun updateState(
39
+ view: PCSegmentedControlView,
40
+ props: ReactStylesDiffMap,
41
+ stateWrapper: StateWrapper
42
+ ): Any? {
43
+ view.stateWrapper = stateWrapper
44
+ return null
45
+ }
46
+
47
+ override fun addEventEmitters(reactContext: ThemedReactContext, view: PCSegmentedControlView) {
48
+ val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)
49
+
50
+ view.onSelect = { index, value ->
51
+ dispatcher?.dispatchEvent(SelectEvent(view.id, index, value))
52
+ }
53
+ }
54
+
55
+ // segments: array of {label, value, disabled, icon}
56
+ override fun setSegments(view: PCSegmentedControlView, value: ReadableArray?) {
57
+ val out = ArrayList<PCSegmentedControlView.Segment>()
58
+ if (value != null) {
59
+ for (i in 0 until value.size()) {
60
+ val m = value.getMap(i) ?: continue
61
+ val label = if (m.hasKey("label") && !m.isNull("label")) m.getString("label") ?: "" else ""
62
+ val segValue = if (m.hasKey("value") && !m.isNull("value")) m.getString("value") ?: "" else ""
63
+ val disabled = m.hasKey("disabled") && !m.isNull("disabled") && m.getString("disabled") == "disabled"
64
+ val icon = if (m.hasKey("icon") && !m.isNull("icon")) m.getString("icon") ?: "" else ""
65
+ out.add(PCSegmentedControlView.Segment(label = label, value = segValue, disabled = disabled, icon = icon))
66
+ }
67
+ }
68
+ view.applySegments(out)
69
+ }
70
+
71
+ override fun setSelectedValue(view: PCSegmentedControlView, value: String?) {
72
+ // Spec sentinel: empty string means "no selection"
73
+ view.applySelectedValue(value ?: "")
74
+ }
75
+
76
+ override fun setInteractivity(view: PCSegmentedControlView, value: String?) {
77
+ view.applyInteractivity(value)
78
+ }
79
+
80
+ override fun setAndroid(view: PCSegmentedControlView, value: ReadableMap?) {
81
+ val selectionRequired = value != null && value.hasKey("selectionRequired") &&
82
+ !value.isNull("selectionRequired") && value.getString("selectionRequired") == "true"
83
+ view.applyAndroidProps(selectionRequired)
84
+ }
85
+
86
+ override fun setIos(view: PCSegmentedControlView, value: ReadableMap?) {
87
+ // Android ignores iOS config
88
+ }
89
+
90
+ // --- Events ---
91
+ private class SelectEvent(
92
+ surfaceId: Int,
93
+ private val index: Int,
94
+ private val value: String
95
+ ) : Event<SelectEvent>(surfaceId) {
96
+ override fun getEventName(): String = "topSelect"
97
+ override fun dispatch(rctEventEmitter: RCTEventEmitter) {
98
+ val payload = com.facebook.react.bridge.Arguments.createMap().apply {
99
+ putInt("index", index)
100
+ putString("value", value)
101
+ }
102
+ rctEventEmitter.receiveEvent(viewTag, eventName, payload)
103
+ }
104
+ }
105
+ }
@@ -14,6 +14,8 @@ class PlatformComponentsViewPackage : ReactPackage {
14
14
  PCSelectionMenuViewManager(),
15
15
  PCDatePickerViewManager(),
16
16
  PCContextMenuViewManager(),
17
+ PCSegmentedControlViewManager(),
18
+ PCLiquidGlassViewManager(),
17
19
  )
18
20
  }
19
21
 
@@ -283,30 +283,33 @@ public final class PCDatePickerView: UIControl,
283
283
  // Prevent "settle" events right as we present.
284
284
  suppressNextChangesBriefly()
285
285
 
286
- // Check if using inline style (full calendar) - needs larger popover size
287
- var isInlineStyle = false
288
- if #available(iOS 13.4, *) {
289
- isInlineStyle = picker.preferredDatePickerStyle == .inline
290
- }
291
-
292
286
  let vc = UIViewController()
293
287
  picker.translatesAutoresizingMaskIntoConstraints = false
294
288
  vc.view.addSubview(picker)
295
289
 
296
- // For inline style, use system background and constrain all edges
297
- // For other styles, use clear background and only top/leading constraints
298
- if isInlineStyle {
290
+ let useInlineFallback: Bool
291
+ if #available(iOS 26.0, *) {
292
+ useInlineFallback = false
293
+ } else {
294
+ useInlineFallback = true
295
+ }
296
+
297
+ if useInlineFallback {
298
+ // Pre–Liquid Glass fallback
299
299
  vc.view.backgroundColor = .systemBackground
300
300
  vc.view.isOpaque = true
301
+
301
302
  NSLayoutConstraint.activate([
302
- picker.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 8),
303
- picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor, constant: 8),
304
- picker.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor, constant: -8),
305
- picker.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: -8),
303
+ picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
304
+ picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
305
+ picker.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor),
306
+ picker.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor),
306
307
  ])
307
308
  } else {
309
+ // Liquid Glass path
308
310
  vc.view.backgroundColor = .clear
309
311
  vc.view.isOpaque = false
312
+
310
313
  NSLayoutConstraint.activate([
311
314
  picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
312
315
  picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
@@ -0,0 +1,10 @@
1
+ // PCLiquidGlass.h
2
+
3
+ #import <React/RCTViewComponentView.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface PCLiquidGlass : RCTViewComponentView
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,140 @@
1
+ // PCLiquidGlass.mm
2
+
3
+ #import "PCLiquidGlass.h"
4
+
5
+ #import <React/RCTComponentViewFactory.h>
6
+ #import <React/RCTConversions.h>
7
+ #import <React/RCTFabricComponentsPlugins.h>
8
+
9
+ #import <react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h>
10
+ #import <react/renderer/components/PlatformComponentsViewSpec/EventEmitters.h>
11
+ #import <react/renderer/components/PlatformComponentsViewSpec/Props.h>
12
+
13
+ #if __has_include(<PlatformComponents/PlatformComponents-Swift.h>)
14
+ #import <PlatformComponents/PlatformComponents-Swift.h>
15
+ #else
16
+ #import "PlatformComponents-Swift.h"
17
+ #endif
18
+
19
+ using namespace facebook::react;
20
+
21
+ @implementation PCLiquidGlass {
22
+ PCLiquidGlassView *_view;
23
+ }
24
+
25
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
26
+ return concreteComponentDescriptorProvider<PCLiquidGlassComponentDescriptor>();
27
+ }
28
+
29
+ - (instancetype)initWithFrame:(CGRect)frame {
30
+ if (self = [super initWithFrame:frame]) {
31
+ _view = [[PCLiquidGlassView alloc] initWithEffect:nil];
32
+ self.contentView = _view;
33
+
34
+ // Set up press callback
35
+ __weak __typeof(self) weakSelf = self;
36
+ _view.onPressCallback = ^(CGFloat x, CGFloat y) {
37
+ __typeof(self) strongSelf = weakSelf;
38
+ if (!strongSelf) return;
39
+
40
+ auto eventEmitter =
41
+ std::static_pointer_cast<const PCLiquidGlassEventEmitter>(
42
+ strongSelf->_eventEmitter);
43
+ if (!eventEmitter) return;
44
+
45
+ PCLiquidGlassEventEmitter::OnGlassPress payload = {
46
+ .x = (float)x,
47
+ .y = (float)y,
48
+ };
49
+ eventEmitter->onGlassPress(payload);
50
+ };
51
+ }
52
+ return self;
53
+ }
54
+
55
+ // Mount children into the UIVisualEffectView's contentView
56
+ - (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
57
+ [_view.contentView insertSubview:childComponentView atIndex:index];
58
+ }
59
+
60
+ - (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index {
61
+ [childComponentView removeFromSuperview];
62
+ }
63
+
64
+ - (void)updateProps:(Props::Shared const &)props
65
+ oldProps:(Props::Shared const &)oldProps {
66
+ const auto &newProps =
67
+ *std::static_pointer_cast<const PCLiquidGlassProps>(props);
68
+ const auto prevProps =
69
+ std::static_pointer_cast<const PCLiquidGlassProps>(oldProps);
70
+
71
+ BOOL needsSetup = NO;
72
+
73
+ // cornerRadius -> glassCornerRadius
74
+ if (!prevProps || newProps.cornerRadius != prevProps->cornerRadius) {
75
+ _view.glassCornerRadius = newProps.cornerRadius;
76
+ }
77
+
78
+ // iOS-specific props
79
+ const auto &newIos = newProps.ios;
80
+ const auto &oldIos = prevProps ? prevProps->ios : PCLiquidGlassIosStruct{};
81
+
82
+ // interactive - needs setup to re-create effect with isInteractive
83
+ if (!prevProps || newIos.interactive != oldIos.interactive) {
84
+ _view.interactive = (newIos.interactive == "true");
85
+ needsSetup = YES;
86
+ }
87
+
88
+ // effect -> effectStyle
89
+ if (!prevProps || newIos.effect != oldIos.effect) {
90
+ if (!newIos.effect.empty()) {
91
+ _view.effectStyle = [NSString stringWithUTF8String:newIos.effect.c_str()];
92
+ } else {
93
+ _view.effectStyle = @"regular";
94
+ }
95
+ needsSetup = YES;
96
+ }
97
+
98
+ // tintColor -> glassTintColor
99
+ if (!prevProps || newIos.tintColor != oldIos.tintColor) {
100
+ if (!newIos.tintColor.empty()) {
101
+ _view.glassTintColor = [NSString stringWithUTF8String:newIos.tintColor.c_str()];
102
+ } else {
103
+ _view.glassTintColor = nil;
104
+ }
105
+ needsSetup = YES;
106
+ }
107
+
108
+ // colorScheme
109
+ if (!prevProps || newIos.colorScheme != oldIos.colorScheme) {
110
+ if (!newIos.colorScheme.empty()) {
111
+ _view.colorScheme = [NSString stringWithUTF8String:newIos.colorScheme.c_str()];
112
+ } else {
113
+ _view.colorScheme = @"system";
114
+ }
115
+ needsSetup = YES;
116
+ }
117
+
118
+ // shadowRadius -> glassShadowRadius
119
+ if (!prevProps || newIos.shadowRadius != oldIos.shadowRadius) {
120
+ _view.glassShadowRadius = newIos.shadowRadius;
121
+ }
122
+
123
+ // isHighlighted
124
+ if (!prevProps || newIos.isHighlighted != oldIos.isHighlighted) {
125
+ _view.isHighlighted = (newIos.isHighlighted == "true");
126
+ }
127
+
128
+ // Apply glass effect if any glass-related props changed
129
+ if (needsSetup) {
130
+ [_view setupView];
131
+ }
132
+
133
+ [super updateProps:props oldProps:oldProps];
134
+ }
135
+
136
+ @end
137
+
138
+ Class<RCTComponentViewProtocol> PCLiquidGlassCls(void) {
139
+ return PCLiquidGlass.class;
140
+ }