craft-native 0.0.251 → 0.0.262

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 (30) hide show
  1. package/dist/android/src/index.d.ts +4 -2
  2. package/dist/android/src/index.js +69 -5
  3. package/dist/android/templates/AndroidManifest.xml.template +1 -0
  4. package/dist/android/templates/CraftHealthServices.kt.template +132 -0
  5. package/dist/android/templates/CraftHealthServicesService.kt.template +217 -0
  6. package/dist/android/templates/CraftHealthServicesStub.kt.template +16 -0
  7. package/dist/android/templates/CraftNativeHealth.kt.template +115 -0
  8. package/dist/android/templates/CraftNativeHealthStub.kt.template +1 -0
  9. package/dist/android/templates/CraftNativeMapGoogle.kt.template +0 -1
  10. package/dist/android/templates/CraftNativeServices.kt.template +31 -8
  11. package/dist/android/templates/CraftNativeShell.kt.template +73 -23
  12. package/dist/android/templates/MainActivityNative.kt.template +217 -28
  13. package/dist/android/templates/build.gradle.kts.app.template +5 -0
  14. package/dist/api/mobile.d.ts +9 -1
  15. package/dist/cli.js +1 -1
  16. package/dist/components/index.d.ts +19 -10
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/ios/templates/CraftApp.swift +36 -4
  20. package/dist/ios/templates/CraftHealth.swift +38 -0
  21. package/dist/ios/templates/CraftHybrid.swift +38 -8
  22. package/dist/ios/templates/CraftNativeActions.swift +1 -1
  23. package/dist/ios/templates/CraftNativeFlatList.swift +34 -20
  24. package/dist/ios/templates/CraftNativeScreen.swift +308 -28
  25. package/dist/ios/templates/testing/DriverTests.swift.template +21 -1
  26. package/dist/mobile-media-testing.d.ts +2 -0
  27. package/dist/mobile-testing.js +19 -10
  28. package/dist/mobile.js +1 -1
  29. package/dist/testing/media-cache/media-cache.kt +47 -6
  30. package/package.json +1 -1
@@ -226,6 +226,8 @@ private data class CraftNativeLayoutStyle(
226
226
  val bottom: Int? = null,
227
227
  val left: Int? = null,
228
228
  val alignSelf: String? = null,
229
+ /** CSS/Yoga order; Kotlin's stable sort preserves source order for ties. */
230
+ val order: Int = 0,
229
231
  val flexGrow: Double = 0.0,
230
232
  val flexShrink: Double? = null,
231
233
  val flexBasis: CraftNativeLength? = null,
@@ -252,6 +254,7 @@ private data class CraftNativeLayoutStyle(
252
254
  position = craftNativeOptionalString(raw.opt("position")) ?: "relative",
253
255
  top = number("top"), right = number("right"), bottom = number("bottom"), left = number("left"),
254
256
  alignSelf = craftNativeOptionalString(raw.opt("alignSelf"))?.takeUnless { it == "auto" },
257
+ order = decimal("order")?.toInt() ?: 0,
255
258
  flexGrow = decimal("flexGrow") ?: flex?.coerceAtLeast(0.0) ?: 0.0,
256
259
  flexShrink = decimal("flexShrink") ?: flex?.let { if (it != 0.0) 1.0 else 0.0 },
257
260
  flexBasis = length("flexBasis")
@@ -759,6 +762,42 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
759
762
  private var safeAreaPadding = intArrayOf(0, 0, 0, 0)
760
763
  private var applyingPadding = false
761
764
  private var safeAreaEdges = setOf("top", "right", "bottom", "left")
765
+ private var shieldedModalTouch = false
766
+ private var dismissibleModalTouch: CraftNativeModalView? = null
767
+
768
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
769
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
770
+ val x = event.x.roundToInt()
771
+ val y = event.y.roundToInt()
772
+ dismissibleModalTouch = (childCount - 1 downTo 0).asSequence()
773
+ .map { getChildAt(it) }
774
+ .filterIsInstance<CraftNativeModalView>()
775
+ .firstOrNull { child ->
776
+ child.dismissOnOutsideTap && child.visibility == View.VISIBLE && child.alpha > 0.01f
777
+ && (x < child.left || x >= child.right || y < child.top || y >= child.bottom)
778
+ }
779
+ shieldedModalTouch = (childCount - 1 downTo 0).any { index ->
780
+ val child = getChildAt(index)
781
+ child is CraftNativeModalView && !child.blocksBackground && child.visibility == View.VISIBLE
782
+ && (x < child.left || x >= child.right || y < child.top || y >= child.bottom)
783
+ && child.hasBlockingModalBelow()
784
+ }
785
+ }
786
+ dismissibleModalTouch?.let { modal ->
787
+ if (event.actionMasked == MotionEvent.ACTION_UP) {
788
+ modal.requestClose(reason = "backdrop")
789
+ dismissibleModalTouch = null
790
+ } else if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
791
+ dismissibleModalTouch = null
792
+ }
793
+ return true
794
+ }
795
+ if (shieldedModalTouch) {
796
+ if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) shieldedModalTouch = false
797
+ return true
798
+ }
799
+ return super.dispatchTouchEvent(event)
800
+ }
762
801
 
763
802
  override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
764
803
  if (!applyingPadding) authoredPadding = intArrayOf(left, top, right, bottom)
@@ -991,6 +1030,11 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
991
1030
  invalidateMeasurementAncestors(parent)
992
1031
  }
993
1032
 
1033
+ /** Flex/grid order with Kotlin's stable sort retaining source order for ties. */
1034
+ private fun orderedChildIndices(): List<Int> = (0 until childCount).sortedWith(
1035
+ compareBy<Int> { styles[getChildAt(it)]?.order ?: 0 }.thenBy { it },
1036
+ )
1037
+
994
1038
  private data class Entry(val view: View, val style: CraftNativeLayoutStyle, val width: Int, val height: Int)
995
1039
  private data class Line(val entries: MutableList<Entry> = mutableListOf(), var main: Float = 0f, var cross: Float = 0f)
996
1040
  private data class GridLayout(val rows: List<List<Entry>>, val absolute: List<Entry>, val cellWidth: Float, val rowHeights: List<Float>)
@@ -1002,7 +1046,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1002
1046
  val lines = mutableListOf<Line>()
1003
1047
  val absolute = mutableListOf<Entry>()
1004
1048
  val pending = mutableListOf<Entry>()
1005
- for (index in 0 until childCount) {
1049
+ for (index in orderedChildIndices()) {
1006
1050
  val child = getChildAt(index)
1007
1051
  if (child.visibility == GONE) continue
1008
1052
  val style = styles[child] ?: CraftNativeLayoutStyle()
@@ -1010,13 +1054,25 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1010
1054
  val horizontalMargins = (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
1011
1055
  val verticalMargins = (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
1012
1056
  if (child is CraftNativeModalView) {
1013
- val modalWidth = max(0, width - paddingLeft - paddingRight)
1014
- val modalHeight = max(0, height - paddingTop - paddingBottom)
1015
- child.measure(
1016
- View.MeasureSpec.makeMeasureSpec(modalWidth, View.MeasureSpec.EXACTLY),
1017
- View.MeasureSpec.makeMeasureSpec(modalHeight, View.MeasureSpec.EXACTLY),
1018
- )
1019
- absolute += Entry(child, style, modalWidth, modalHeight)
1057
+ if (child.blocksBackground) {
1058
+ val modalWidth = max(0, width - paddingLeft - paddingRight)
1059
+ val modalHeight = max(0, height - paddingTop - paddingBottom)
1060
+ child.measure(
1061
+ View.MeasureSpec.makeMeasureSpec(modalWidth, View.MeasureSpec.EXACTLY),
1062
+ View.MeasureSpec.makeMeasureSpec(modalHeight, View.MeasureSpec.EXACTLY),
1063
+ )
1064
+ absolute += Entry(child, style, modalWidth, modalHeight)
1065
+ } else {
1066
+ // A non-modal popup is still promoted to the screen root,
1067
+ // but its authored absolute style controls its viewport
1068
+ // frame instead of turning it into a full-screen child.
1069
+ // The root content box is the same screenViewport origin
1070
+ // returned by View.measure; safe-area padding is not
1071
+ // injected a second time here.
1072
+ val childWidth = measureChildWidth(child, style, width - paddingLeft - paddingRight, horizontalMargins)
1073
+ val childHeight = measureChildHeight(child, style, height - paddingTop - paddingBottom, verticalMargins)
1074
+ absolute += Entry(child, style, childWidth, childHeight)
1075
+ }
1020
1076
  continue
1021
1077
  }
1022
1078
  val childWidth = measureChildWidth(child, style, width - paddingLeft - paddingRight, horizontalMargins)
@@ -1101,7 +1157,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1101
1157
  val cellWidth = max(0f, (availableWidth.toFloat() - max(0, gridColumns - 1) * gap) / max(1, gridColumns).toFloat())
1102
1158
  val rows = mutableListOf<MutableList<Entry>>()
1103
1159
  val absolute = mutableListOf<Entry>()
1104
- for (index in 0 until childCount) {
1160
+ for (index in orderedChildIndices()) {
1105
1161
  val child = getChildAt(index)
1106
1162
  if (child.visibility == GONE) continue
1107
1163
  val style = styles[child] ?: CraftNativeLayoutStyle()
@@ -1109,13 +1165,19 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1109
1165
  val horizontalMargins = (lp?.leftMargin ?: 0) + (lp?.rightMargin ?: 0)
1110
1166
  val verticalMargins = (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
1111
1167
  if (child is CraftNativeModalView) {
1112
- val modalWidth = max(0, width - paddingLeft - paddingRight)
1113
- val modalHeight = max(0, height - paddingTop - paddingBottom)
1114
- child.measure(
1115
- View.MeasureSpec.makeMeasureSpec(modalWidth, View.MeasureSpec.EXACTLY),
1116
- View.MeasureSpec.makeMeasureSpec(modalHeight, View.MeasureSpec.EXACTLY),
1117
- )
1118
- absolute += Entry(child, style, modalWidth, modalHeight)
1168
+ if (child.blocksBackground) {
1169
+ val modalWidth = max(0, width - paddingLeft - paddingRight)
1170
+ val modalHeight = max(0, height - paddingTop - paddingBottom)
1171
+ child.measure(
1172
+ View.MeasureSpec.makeMeasureSpec(modalWidth, View.MeasureSpec.EXACTLY),
1173
+ View.MeasureSpec.makeMeasureSpec(modalHeight, View.MeasureSpec.EXACTLY),
1174
+ )
1175
+ absolute += Entry(child, style, modalWidth, modalHeight)
1176
+ } else {
1177
+ val childWidth = measureChildWidth(child, style, width - paddingLeft - paddingRight, horizontalMargins)
1178
+ val childHeight = measureChildHeight(child, style, height - paddingTop - paddingBottom, verticalMargins)
1179
+ absolute += Entry(child, style, childWidth, childHeight)
1180
+ }
1119
1181
  continue
1120
1182
  }
1121
1183
  val childWidth = measureChildWidth(child, style, cellWidth.roundToInt(), horizontalMargins)
@@ -1136,9 +1198,9 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1136
1198
  val rowHeights = if (gridRows != null) {
1137
1199
  val count = max(gridRows ?: 0, naturalRowHeights.size)
1138
1200
  while (rows.size < count) rows.add(mutableListOf())
1139
- val available = max(0f, height - paddingTop - paddingBottom - max(0, count - 1) * rowGap)
1201
+ val available = max(0f, (height - paddingTop - paddingBottom).toFloat() - max(0, count - 1) * rowGap)
1140
1202
  val equal = available / max(1, count).toFloat()
1141
- (naturalRowHeights + List(count - naturalRowHeights.size) { equal })
1203
+ (naturalRowHeights + List(count - naturalRowHeights.size) { 0f })
1142
1204
  .map { max(it, equal) }
1143
1205
  } else naturalRowHeights
1144
1206
  return GridLayout(rows, absolute, cellWidth, rowHeights)
@@ -1149,7 +1211,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1149
1211
  val columnWidths = IntArray(columns)
1150
1212
  val rowHeights = mutableListOf<Int>()
1151
1213
  var flowIndex = 0
1152
- for (index in 0 until childCount) {
1214
+ for (index in orderedChildIndices()) {
1153
1215
  val child = getChildAt(index)
1154
1216
  if (child.visibility == GONE) continue
1155
1217
  val style = styles[child] ?: CraftNativeLayoutStyle()
@@ -1438,12 +1500,12 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1438
1500
  put(base, 26, node.padding[3].toFloat())
1439
1501
  put(base, 27, node.padding[0].toFloat())
1440
1502
  putEnum(base, 28, if (node.grid) 1 else 0)
1441
- put(base, 29, node.gridColumns.toFloat())
1503
+ putEnum(base, 29, node.gridColumns)
1442
1504
  put(base, 30, node.rowGap)
1443
1505
  put(base, 31, node.columnGap)
1444
1506
  if (fields >= 33) putEnum(base, 32, (if (node.reverse) 1 else 0) or (if (node.wrapReverse) 2 else 0))
1445
1507
  if (autoRowsAware) put(base, 33, sharedLength(node.gridAutoRows, contentHeight))
1446
- if (explicitRowsAware) put(base, 34, (node.gridRows ?: 0).toFloat())
1508
+ if (explicitRowsAware) putEnum(base, 34, node.gridRows ?: 0)
1447
1509
  val firstChild = cursor
1448
1510
  node.children.forEach { child -> write(child, contentWidth, contentHeight, false, node.direction) }
1449
1511
  treeMeta[index * 2] = firstChild.toFloat()
@@ -1527,7 +1589,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1527
1589
  put(base, 26, padding[3].toFloat())
1528
1590
  put(base, 27, padding[0].toFloat())
1529
1591
  putEnum(base, 28, 0)
1530
- put(base, 29, 1f)
1592
+ putEnum(base, 29, 1)
1531
1593
  put(base, 30, if (root) rowGapValue else 0f)
1532
1594
  put(base, 31, if (root) columnGapValue else 0f)
1533
1595
  }
@@ -1796,7 +1858,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
1796
1858
  if (absolute.isEmpty() && lines.isNotEmpty() && (lines.size == 1 || wrap)) {
1797
1859
  val sharedEntries = if (wrap) lines.flatMap { it.entries } else lines[0].entries
1798
1860
  val sharedTree = sharedTreeFrames(sharedEntries, width, height)
1799
- if (sharedTree != null && (sharedTree.nodes.size > sharedEntries.size + 1 || grid)) {
1861
+ if (sharedTree != null && (sharedTree.nodes.size > sharedEntries.size + 1 || grid || reverse || wrapReverse || gridAutoRows != null || gridRows != null)) {
1800
1862
  for (index in 1 until sharedTree.nodes.size) {
1801
1863
  val view = sharedTree.nodes[index].view ?: continue
1802
1864
  val offset = index * 4
@@ -2560,6 +2622,8 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
2560
2622
  private var animationGeneration = 0
2561
2623
  var focusToRestore: View? = null
2562
2624
  var onRequestClose: ((String) -> Unit)? = null
2625
+ var viewportStyle = JSONObject()
2626
+ var declarationPath = ""
2563
2627
 
2564
2628
  var transparent: Boolean = false
2565
2629
  set(value) {
@@ -2567,6 +2631,18 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
2567
2631
  blocker.setBackgroundColor(if (value) Color.TRANSPARENT else Color.argb(82, 0, 0, 0))
2568
2632
  }
2569
2633
 
2634
+ /** Existing Modal nodes block by default; non-modal popups let outside taps through. */
2635
+ var blocksBackground: Boolean = true
2636
+ set(value) {
2637
+ field = value
2638
+ isClickable = value
2639
+ blocker.isClickable = value
2640
+ blocker.isEnabled = value
2641
+ }
2642
+
2643
+ /** A non-modal popup may opt into consuming an outside tap for dismissal. */
2644
+ var dismissOnOutsideTap: Boolean = false
2645
+
2570
2646
  init {
2571
2647
  isClickable = true
2572
2648
  blocker.isClickable = true
@@ -2580,6 +2656,7 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
2580
2656
  }
2581
2657
 
2582
2658
  fun requestClose(reason: String = "backdrop") {
2659
+ if (!blocksBackground && !dismissOnOutsideTap) return
2583
2660
  val handler = onRequestClose ?: return
2584
2661
  isClickable = false
2585
2662
  blocker.isEnabled = false
@@ -2588,8 +2665,8 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
2588
2665
 
2589
2666
  fun setVisible(visible: Boolean, opacity: Float, animationType: String?) {
2590
2667
  if (visible) {
2591
- isClickable = true
2592
- blocker.isEnabled = true
2668
+ isClickable = blocksBackground
2669
+ blocker.isEnabled = blocksBackground
2593
2670
  }
2594
2671
  if (renderedVisible == visible && visibility == if (visible) View.VISIBLE else View.GONE) {
2595
2672
  if (visible && alpha != opacity) alpha = opacity
@@ -2643,9 +2720,33 @@ private class CraftNativeModalView(context: Context) : FrameLayout(context) {
2643
2720
  translationY = 0f
2644
2721
  alpha = 0f
2645
2722
  isClickable = true
2723
+ blocksBackground = true
2724
+ dismissOnOutsideTap = false
2646
2725
  blocker.isEnabled = true
2647
2726
  onRequestClose = null
2648
2727
  }
2728
+
2729
+ override fun onTouchEvent(event: MotionEvent): Boolean {
2730
+ if (!blocksBackground) {
2731
+ if (dismissOnOutsideTap && event.action == MotionEvent.ACTION_UP) {
2732
+ requestClose(reason = "backdrop")
2733
+ return true
2734
+ }
2735
+ // Do not let an outside popup tap fall through to a blocking modal
2736
+ // below it; a normal non-modal popup remains pass-through.
2737
+ return hasBlockingModalBelow()
2738
+ }
2739
+ return super.onTouchEvent(event)
2740
+ }
2741
+
2742
+ fun hasBlockingModalBelow(): Boolean {
2743
+ val group = parent as? ViewGroup ?: return false
2744
+ val index = group.indexOfChild(this)
2745
+ return index > 0 && (0 until index).any { siblingIndex ->
2746
+ val sibling = group.getChildAt(siblingIndex)
2747
+ sibling is CraftNativeModalView && sibling.blocksBackground && sibling.visibility == View.VISIBLE && sibling.alpha > 0.01f
2748
+ }
2749
+ }
2649
2750
  }
2650
2751
 
2651
2752
  private data class LayoutBounds(
@@ -2902,7 +3003,8 @@ internal class CraftNativeInputEcho {
2902
3003
  * A native screen's `<Video>`: the platform's VideoView.
2903
3004
  *
2904
3005
  * It plays without taking audio focus, so the person's music carries on
2905
- * under it, as on iOS. `onLoad`, `onPlay`, `onPause`, `onEnd` and `onError`
3006
+ * under it, as on iOS. `onLoadStart`, `onLoad`, `onLoadEnd`, `onPlay`,
3007
+ * `onPause`, `onEnd` and `onError`
2906
3008
  * reach the screen in the web's names, from the controls as well as from
2907
3009
  * props. With `pictureInPicture` the activity takes it into a floating
2908
3010
  * window when the person leaves the app while it plays (MainActivity
@@ -2918,6 +3020,8 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
2918
3020
  private var ended = false
2919
3021
  private var muted = false
2920
3022
  private var resizeMode = "contain"
3023
+ private var requestedTimeSeconds: Double? = null
3024
+ private var pendingSeekSeconds: Double? = null
2921
3025
  private var preparedPlayer: MediaPlayer? = null
2922
3026
  /** The screen's handlers by event name, and how an event reaches them. */
2923
3027
  var handlers: JSONObject = JSONObject()
@@ -2946,12 +3050,25 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
2946
3050
  setAudioFocusRequest(AudioManager.AUDIOFOCUS_NONE)
2947
3051
  }
2948
3052
 
2949
- fun configure(source: Any?, autoplay: Boolean, paused: Boolean, loop: Boolean, muted: Boolean, controls: Boolean, resizeMode: String?, pictureInPicture: Boolean = false) {
3053
+ private fun applyPendingSeek() {
3054
+ val target = pendingSeekSeconds ?: return
3055
+ if (preparedPlayer == null) return
3056
+ val current = currentPosition.coerceAtLeast(0) / 1000.0
3057
+ pendingSeekSeconds = null
3058
+ if (kotlin.math.abs(current - target) > 0.25) seekTo((target * 1000.0).roundToInt().coerceAtLeast(0))
3059
+ }
3060
+
3061
+ fun configure(source: Any?, autoplay: Boolean, paused: Boolean, loop: Boolean, muted: Boolean, currentTime: Any?, controls: Boolean, resizeMode: String?, pictureInPicture: Boolean = false) {
2950
3062
  val uri = when (source) {
2951
3063
  is String -> source
2952
3064
  is JSONObject -> craftNativeOptionalString(source.opt("uri")).orEmpty()
2953
3065
  else -> ""
2954
3066
  }
3067
+ val nextTime = craftNativeNumber(currentTime)?.takeIf { it.isFinite() && it >= 0.0 }
3068
+ if (nextTime != requestedTimeSeconds) {
3069
+ requestedTimeSeconds = nextTime
3070
+ pendingSeekSeconds = nextTime
3071
+ }
2955
3072
  shouldLoop = loop
2956
3073
  val nextShouldPlay = autoplay && !paused
2957
3074
  if (playIntent != nextShouldPlay) {
@@ -2971,6 +3088,7 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
2971
3088
  setMediaController(if (controls) MediaController(context).also { it.setAnchorView(this) } else null)
2972
3089
  if (uri != sourceUri) {
2973
3090
  sourceUri = uri
3091
+ pendingSeekSeconds = requestedTimeSeconds
2974
3092
  userPaused = false
2975
3093
  ended = false
2976
3094
  stopPlayback()
@@ -2980,6 +3098,7 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
2980
3098
  // A copy kept on the phone (craft.media.keep) plays in place of
2981
3099
  // the address: offline in a gym with no signal.
2982
3100
  val kept = CraftNativeMedia.local(context, uri)
3101
+ fire("onLoadStart")
2983
3102
  setVideoURI(if (kept != null) Uri.fromFile(kept) else Uri.parse(uri))
2984
3103
  setOnPreparedListener { mediaPlayer ->
2985
3104
  preparedPlayer = mediaPlayer
@@ -2987,7 +3106,9 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
2987
3106
  mediaPlayer.isLooping = shouldLoop
2988
3107
  val volume = if (this.muted) 0f else 1f
2989
3108
  mediaPlayer.setVolume(volume, volume)
3109
+ applyPendingSeek()
2990
3110
  fire("onLoad", JSONObject().put("duration", duration / 1000.0))
3111
+ fire("onLoadEnd")
2991
3112
  if (shouldPlay) start()
2992
3113
  }
2993
3114
  setOnCompletionListener {
@@ -3005,6 +3126,7 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
3005
3126
  }
3006
3127
  }
3007
3128
  if (uri.isBlank()) return
3129
+ applyPendingSeek()
3008
3130
  runCatching {
3009
3131
  if (shouldPlay && !isPlaying && !userPaused && !ended) start()
3010
3132
  else if (!shouldPlay && isPlaying) pause()
@@ -3074,6 +3196,8 @@ private class CraftNativeVideoView(context: Context) : VideoView(context) {
3074
3196
  stopPlayback()
3075
3197
  setPlaying(false)
3076
3198
  sourceUri = ""
3199
+ requestedTimeSeconds = null
3200
+ pendingSeekSeconds = null
3077
3201
  playIntent = null
3078
3202
  userPaused = false
3079
3203
  ended = false
@@ -4619,6 +4743,7 @@ class MainActivity : AppCompatActivity() {
4619
4743
  put("viewMeasure")
4620
4744
  put("viewScroll")
4621
4745
  put("viewReparent")
4746
+ put("viewNonModalPresentation")
4622
4747
  }
4623
4748
 
4624
4749
  private fun appVersion(): Pair<String, String> = runCatching {
@@ -4906,6 +5031,9 @@ class MainActivity : AppCompatActivity() {
4906
5031
  if (!updateParentLayoutStyle(updated, document.optJSONObject("style") ?: JSONObject())) return false
4907
5032
  }
4908
5033
  if (applied.isNotEmpty()) {
5034
+ // Updating a modal's content may reattach its nested popups to
5035
+ // their declaration parent. Restore the screen overlay contract.
5036
+ promoteModalOverlays(screen)
4909
5037
  scheduleLayoutEvents(screen)
4910
5038
  }
4911
5039
  return true
@@ -4995,6 +5123,7 @@ class MainActivity : AppCompatActivity() {
4995
5123
  screen.controls.putAll(next)
4996
5124
  screen.handlers.keys.retainAll(retainedViews(screen))
4997
5125
  screen.view.setChildStyle(tree, document.optJSONObject("style") ?: JSONObject(), resources.displayMetrics.density)
5126
+ promoteModalOverlays(screen)
4998
5127
  updateSafeAreaViews()
4999
5128
  // A full render can change a view's custom layout style without
5000
5129
  // changing the root bounds. Ensure native @layout observers still see
@@ -5003,6 +5132,45 @@ class MainActivity : AppCompatActivity() {
5003
5132
  if (visibleScreen === screen) applyStatusBar(screen)
5004
5133
  }
5005
5134
 
5135
+ /** Keep Modal nodes in the screen viewport even when declared in a clipped subtree. */
5136
+ private fun promoteModalOverlays(screen: Screen) {
5137
+ if (screen.modalVisibility.isEmpty()) return
5138
+ // Snapshot parent-before-child order before moving anything. The
5139
+ // controls map registers descendants first, which would place a
5140
+ // parent modal above its own popup when brought forward in that order.
5141
+ val overlays = mutableListOf<CraftNativeModalView>()
5142
+ fun collect(view: View) {
5143
+ if (view is CraftNativeModalView) overlays.add(view)
5144
+ if (view is ViewGroup) for (index in 0 until view.childCount) collect(view.getChildAt(index))
5145
+ }
5146
+ collect(screen.view)
5147
+ // A previous targeted update may already have promoted siblings in a
5148
+ // different order. Use declaration paths, with numeric child indices,
5149
+ // so a nested popup stays above its parent after every update.
5150
+ overlays.sortWith(Comparator { first, second ->
5151
+ val firstPath = first.declarationPath.split('.')
5152
+ val secondPath = second.declarationPath.split('.')
5153
+ for (index in 0 until min(firstPath.size, secondPath.size)) {
5154
+ val left = firstPath[index]
5155
+ val right = secondPath[index]
5156
+ val leftIndex = left.toIntOrNull()
5157
+ val rightIndex = right.toIntOrNull()
5158
+ val order = if (leftIndex != null && rightIndex != null) leftIndex.compareTo(rightIndex) else left.compareTo(right)
5159
+ if (order != 0) return@Comparator order
5160
+ }
5161
+ firstPath.size.compareTo(secondPath.size)
5162
+ })
5163
+ overlays.forEach { modal ->
5164
+ if (modal.parent !== screen.view) {
5165
+ (modal.parent as? ViewGroup)?.removeView(modal)
5166
+ screen.view.addView(modal)
5167
+ // Removing a view clears its old parent's flex style.
5168
+ screen.view.setChildStyle(modal, modal.viewportStyle, resources.displayMetrics.density)
5169
+ }
5170
+ modal.bringToFront()
5171
+ }
5172
+ }
5173
+
5006
5174
  private fun applyStatusBar(screen: Screen) {
5007
5175
  val controller = WindowCompat.getInsetsController(window, window.decorView)
5008
5176
  val night = (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
@@ -5365,6 +5533,15 @@ class MainActivity : AppCompatActivity() {
5365
5533
  "Modal" -> (previous as? CraftNativeModalView ?: CraftNativeModalView(this)).apply {
5366
5534
  val focusBeforeContent = screen.view.findFocus()
5367
5535
  transparent = craftNativeBoolean(props.opt("transparent"))
5536
+ blocksBackground = !craftNativeBoolean(props.opt("nonModal"))
5537
+ dismissOnOutsideTap = craftNativeBoolean(props.opt("dismissOnOutsideTap"))
5538
+ // Modal nodes are promoted from their declaration parent to
5539
+ // the screen root. Keep the authored layout style with that
5540
+ // promoted child so non-modal absolute coordinates remain in
5541
+ // the screen viewport rather than the old clipped parent.
5542
+ viewportStyle = style
5543
+ declarationPath = path
5544
+ screen.view.setChildStyle(this, style, resources.displayMetrics.density)
5368
5545
  content.configure(style, resources.displayMetrics.density)
5369
5546
  reconcileChildren(screen, content, children, identity, next, style, previousControls)
5370
5547
  // Dynamic :visible bindings arrive as a display style mutation;
@@ -5598,6 +5775,7 @@ class MainActivity : AppCompatActivity() {
5598
5775
  paused = craftNativeBoolean(props.opt("paused")),
5599
5776
  loop = craftNativeBoolean(props.opt("loop")),
5600
5777
  muted = craftNativeBoolean(props.opt("muted")),
5778
+ currentTime = props.opt("currentTime"),
5601
5779
  controls = craftNativeBoolean(props.opt("controls")),
5602
5780
  resizeMode = craftNativeResizeMode(style, props),
5603
5781
  pictureInPicture = craftNativeBoolean(props.opt("pictureInPicture")),
@@ -6001,6 +6179,7 @@ class MainActivity : AppCompatActivity() {
6001
6179
  if (layout.getChildAt(index) is CraftNativeFlexSpacer) layout.removeViewAt(index)
6002
6180
  }
6003
6181
  val desired = mutableListOf<View>()
6182
+ val desiredOrder = mutableMapOf<View, Int>()
6004
6183
  for (index in 0 until children.length()) {
6005
6184
  val child = children.opt(index)
6006
6185
  val childView = when (child) {
@@ -6013,9 +6192,12 @@ class MainActivity : AppCompatActivity() {
6013
6192
  }
6014
6193
  if (childView != null) desired.add(childView)
6015
6194
  if (childView != null && child is JSONObject) {
6016
- layout.setChildStyle(childView, child.optJSONObject("style") ?: JSONObject(), resources.displayMetrics.density)
6195
+ val childStyle = child.optJSONObject("style") ?: JSONObject()
6196
+ layout.setChildStyle(childView, childStyle, resources.displayMetrics.density)
6197
+ desiredOrder[childView] = craftNativeFiniteNumber(childStyle, "order", 0.0).toInt()
6017
6198
  }
6018
6199
  }
6200
+ desired.sortWith(compareBy { desiredOrder[it] ?: 0 })
6019
6201
  if (craftNativeOptionalString(style.opt("flexDirection"))?.endsWith("-reverse") == true) desired.reverse()
6020
6202
  val desiredSet = desired.toSet()
6021
6203
  for (index in layout.childCount - 1 downTo 0) {
@@ -6754,6 +6936,7 @@ class MainActivity : AppCompatActivity() {
6754
6936
  linkedControl -> "link"
6755
6937
  type in setOf("TouchableOpacity", "TouchableHighlight", "Pressable") -> "button"
6756
6938
  type == "Icon" -> "image"
6939
+ type in setOf("FlatList", "SectionList") -> "list"
6757
6940
  else -> type.lowercase()
6758
6941
  }
6759
6942
  }
@@ -7467,6 +7650,12 @@ class MainActivity : AppCompatActivity() {
7467
7650
  // released/recycled rows so a later identity cannot inherit a closure
7468
7651
  // that still captures the previous row's screen state.
7469
7652
  released.forEach {
7653
+ // Modals are promoted out of their logical layout into the
7654
+ // screen overlay. Removing the logical node therefore does not
7655
+ // remove the native view through ordinary child reconciliation;
7656
+ // detach every released view so an open popup cannot survive as
7657
+ // an orphan after its owner unmounts.
7658
+ (it.parent as? ViewGroup)?.removeView(it)
7470
7659
  // AdapterView (including Spinner, our native Picker) rejects
7471
7660
  // setOnClickListener altogether. Its selection callback is
7472
7661
  // detached below; ordinary Views still need their click target
@@ -111,6 +111,11 @@ dependencies {
111
111
  // Android Health Connect (included only when enabled)
112
112
  {{HEALTH_CONNECT_DEPENDENCIES}}
113
113
 
114
+ // Android Health Services live exercise tracking (included only when enabled)
115
+ {{HEALTH_SERVICES_DEPENDENCIES}}
116
+
117
+ {{WALLET_DEPENDENCY}}
118
+
114
119
  // On-device computer vision APIs exposed by CraftBridge
115
120
  implementation("com.google.mlkit:barcode-scanning:17.3.0")
116
121
  implementation("com.google.mlkit:image-labeling:17.0.9")
@@ -1227,6 +1227,11 @@ export interface NativeTabBarOptions {
1227
1227
  tintDark?: string;
1228
1228
  /** Whether Craft's horizontal tab-switch gesture may begin over the page. Defaults to true. */
1229
1229
  swipeEnabled?: boolean;
1230
+ /** The selected tab highlight. Omit to keep the default capsule. */
1231
+ selection?: {
1232
+ /** Continuous corner radius in points. The highlight height is the upper bound. */
1233
+ cornerRadius?: number;
1234
+ };
1230
1235
  }
1231
1236
  /**
1232
1237
  * The phone's own tab bar, floating over the page: Liquid Glass on iOS 26 and
@@ -1698,10 +1703,13 @@ export interface HealthLiveWorkoutResult {
1698
1703
  }
1699
1704
  export interface HealthLiveWorkoutEvent {
1700
1705
  id: string;
1701
- event: 'state' | 'error';
1706
+ event: 'state' | 'metrics' | 'error';
1702
1707
  state?: 'notStarted' | 'prepared' | 'running' | 'paused' | 'stopped' | 'ended' | 'unknown';
1703
1708
  /** Epoch milliseconds for a state transition. */
1704
1709
  date?: number;
1710
+ heartRateBpm?: number;
1711
+ activeEnergyCalories?: number;
1712
+ distanceMeters?: number;
1705
1713
  message?: string;
1706
1714
  }
1707
1715
  /** The health reads and writes a host may or may not have, depending on the app build it runs in. */
package/dist/cli.js CHANGED
@@ -2842,7 +2842,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
2842
2842
  `);
2843
2843
  }
2844
2844
  // package.json
2845
- var version = "0.0.251";
2845
+ var version = "0.0.262";
2846
2846
 
2847
2847
  // bin/cli.ts
2848
2848
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -38,6 +38,8 @@ export interface FlexStyle {
38
38
  flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
39
39
  /** Flex wrap */
40
40
  flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
41
+ /** Flex/grid sibling order; equal values retain source order */
42
+ order?: number;
41
43
  /** Justify content (main axis) */
42
44
  justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
43
45
  /** Align items (cross axis); baseline aligns text-bearing children by their first baseline */
@@ -309,6 +311,8 @@ export interface BaseProps {
309
311
  accessibilityState?: AccessibilityState;
310
312
  /** Accessibility role */
311
313
  accessibilityRole?: 'none' | 'button' | 'link' | 'image' | 'text' | 'header' | 'search' | 'adjustable' | 'menu' | 'summary' | 'alert' | 'checkbox' | 'combobox' | 'menuitem' | 'progressbar' | 'radio' | 'tablist' | 'radiogroup' | 'list' | 'menubar' | 'spinbutton' | 'switch' | 'tab' | 'timer' | 'toolbar' | 'scrollbar';
314
+ /** Native list chrome slot for keyed FlatList/SectionList children. */
315
+ listRole?: 'header' | 'item' | 'separator' | 'empty' | 'footer';
312
316
  /** Matches a view across an animated state change. */
313
317
  transitionID?: string;
314
318
  /** Children elements */
@@ -454,8 +458,14 @@ export interface VideoProps extends BaseProps {
454
458
  autoPlay?: boolean;
455
459
  loop?: boolean;
456
460
  muted?: boolean;
461
+ /** Controlled playback position in seconds. */
462
+ currentTime?: number;
457
463
  paused?: boolean;
458
464
  resizeMode?: 'contain' | 'cover' | 'stretch';
465
+ /** The source began loading. */
466
+ onLoadStart?: () => void;
467
+ /** The source became ready for playback. */
468
+ onLoadEnd?: () => void;
459
469
  /**
460
470
  * Carries on in a floating window (Picture in Picture) when the app goes to
461
471
  * the background or another app opens while it plays, and comes back into
@@ -848,16 +858,7 @@ export interface ActivityIndicatorProps extends BaseProps {
848
858
  * FlatList component props.
849
859
  */
850
860
  export interface FlatListProps<T> extends ScrollViewBaseProps {
851
- /** Data array */
852
- data: T[];
853
- /** Render item function */
854
- renderItem: (info: {
855
- item: T;
856
- index: number;
857
- }) => any;
858
- /** Key extractor function */
859
- keyExtractor?: (item: T, index: number) => string;
860
- /** Number of columns */
861
+ /** Number of columns in the keyed child tree. */
861
862
  numColumns?: number;
862
863
  /** Render rows in reverse order along the scroll axis. */
863
864
  inverted?: boolean;
@@ -870,6 +871,10 @@ export interface FlatListProps<T> extends ScrollViewBaseProps {
870
871
  /** Refreshing state */
871
872
  refreshing?: boolean;
872
873
  }
874
+ /**
875
+ * @deprecated Grouped SectionList data is deferred until the versioned native
876
+ * list-data protocol exists. Use keyed child rows with `listRole` instead.
877
+ */
873
878
  export interface SectionListSection<T> {
874
879
  data: T[];
875
880
  key?: string;
@@ -881,6 +886,10 @@ export interface ModalProps extends BaseProps {
881
886
  style?: StyleValue<ViewStyle>;
882
887
  visible?: boolean;
883
888
  transparent?: boolean;
889
+ /** Keep the popup interactive without blocking the screen behind it. */
890
+ nonModal?: boolean;
891
+ /** When non-modal, consume an outside tap and call onRequestClose. */
892
+ dismissOnOutsideTap?: boolean;
884
893
  animationType?: 'none' | 'fade' | 'slide';
885
894
  onShow?: () => void;
886
895
  onDismiss?: () => void;
package/dist/index.cjs CHANGED
@@ -373,7 +373,7 @@ function craftBinaryNotFoundMessage(triedPath) {
373
373
  `);
374
374
  }
375
375
  // package.json
376
- var version = "0.0.251";
376
+ var version = "0.0.262";
377
377
 
378
378
  // src/sdk-version.ts
379
379
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
117
117
  `);
118
118
  }
119
119
  // package.json
120
- var version = "0.0.251";
120
+ var version = "0.0.262";
121
121
 
122
122
  // src/sdk-version.ts
123
123
  var SDK_VERSION = version;