craft-native 0.0.207 → 0.0.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/templates/CraftNativeShell.kt.template +43 -12
- package/dist/android/templates/MainActivityNative.kt.template +72 -66
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +35 -12
- package/dist/index.cjs +14 -5
- package/dist/index.js +14 -5
- package/dist/ios/templates/CraftApp.swift +48 -8
- package/dist/ios/templates/CraftNativeScreen.swift +41 -6
- package/package.json +1 -1
|
@@ -389,6 +389,10 @@ internal object CraftNativePrelude {
|
|
|
389
389
|
routePrelude: String,
|
|
390
390
|
): String = """
|
|
391
391
|
$routePrelude
|
|
392
|
+
// JavaScriptSandbox has no DOM window, but browser-safe SDK bridges
|
|
393
|
+
// address the host through `window.craft`. Keep native screens
|
|
394
|
+
// compatible without pretending a document exists.
|
|
395
|
+
if (typeof globalThis.window === 'undefined') globalThis.window = globalThis;
|
|
392
396
|
(function() {
|
|
393
397
|
var forward = null;
|
|
394
398
|
var queued = [];
|
|
@@ -567,7 +571,7 @@ internal object CraftNativePrelude {
|
|
|
567
571
|
* The bar above a screen (back, title, the screen's buttons) and the tab bar
|
|
568
572
|
* below it, in plain Android views. A screen sets its bar with
|
|
569
573
|
* `craft.navigation.setOptions` (title, largeTitle, hidden, rightButtons,
|
|
570
|
-
* tabBarHidden); the tabs come from the app's `tabs`.
|
|
574
|
+
* tabAccessory, tabBarHidden); the tabs come from the app's `tabs`.
|
|
571
575
|
*/
|
|
572
576
|
internal class CraftNativeChrome(context: Context, private val tabs: List<CraftNativeTab>) : LinearLayout(context) {
|
|
573
577
|
val content = FrameLayout(context)
|
|
@@ -578,6 +582,11 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
578
582
|
private val largeTitleView = TextView(context)
|
|
579
583
|
private val buttons = LinearLayout(context)
|
|
580
584
|
private val tabBar = LinearLayout(context)
|
|
585
|
+
private val tabRow = LinearLayout(context)
|
|
586
|
+
private val accessory = FrameLayout(context)
|
|
587
|
+
private val accessoryIcon = ImageView(context)
|
|
588
|
+
private val accessoryBadge = TextView(context)
|
|
589
|
+
private var accessorySpec: JSONObject? = null
|
|
581
590
|
private val tabViews = mutableListOf<Pair<TextView, TextView>>()
|
|
582
591
|
private val tabIconText = mutableMapOf<String, String>()
|
|
583
592
|
private val tabImageURLs = mutableMapOf<String, String>()
|
|
@@ -596,6 +605,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
596
605
|
var onBack: (() -> Unit)? = null
|
|
597
606
|
var onButton: ((String) -> Unit)? = null
|
|
598
607
|
var onTab: ((Int) -> Unit)? = null
|
|
608
|
+
var onAccessory: ((String) -> Unit)? = null
|
|
599
609
|
var onTabSwipeStart: ((Float, Float, Float) -> Boolean)? = null
|
|
600
610
|
var onTabSwipeMove: ((Float) -> Unit)? = null
|
|
601
611
|
var onTabSwipeEnd: ((Float, Boolean) -> Unit)? = null
|
|
@@ -660,8 +670,22 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
660
670
|
tabBar.addView(cell, LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f))
|
|
661
671
|
tabViews.add(icon to label)
|
|
662
672
|
}
|
|
663
|
-
|
|
664
|
-
|
|
673
|
+
tabRow.orientation = HORIZONTAL
|
|
674
|
+
tabRow.gravity = Gravity.CENTER_VERTICAL
|
|
675
|
+
tabRow.setPadding(dp(12), dp(4), dp(12), dp(4))
|
|
676
|
+
tabRow.addView(tabBar, LayoutParams(0, dp(64), 1f))
|
|
677
|
+
accessory.addView(accessoryIcon, FrameLayout.LayoutParams(dp(26), dp(26), Gravity.CENTER))
|
|
678
|
+
accessoryBadge.textSize = 10f
|
|
679
|
+
accessoryBadge.gravity = Gravity.CENTER
|
|
680
|
+
accessoryBadge.setTextColor(Color.WHITE)
|
|
681
|
+
accessoryBadge.setPadding(dp(4), 0, dp(4), 0)
|
|
682
|
+
accessoryBadge.background = GradientDrawable().apply { setColor(Color.RED); cornerRadius = dp(9).toFloat() }
|
|
683
|
+
accessory.addView(accessoryBadge, FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, dp(18), Gravity.TOP or Gravity.END))
|
|
684
|
+
accessory.setOnClickListener { accessorySpec?.optString("path")?.let { onAccessory?.invoke(it) } }
|
|
685
|
+
accessory.visibility = View.GONE
|
|
686
|
+
tabRow.addView(accessory, LayoutParams(dp(64), dp(64)).apply { marginStart = dp(8) })
|
|
687
|
+
addView(tabRow, LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(72)))
|
|
688
|
+
tabRow.visibility = if (tabs.isEmpty()) View.GONE else View.VISIBLE
|
|
665
689
|
refreshColors()
|
|
666
690
|
}
|
|
667
691
|
|
|
@@ -671,7 +695,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
671
695
|
val overHorizontalScroll = onTabSwipeShouldDefer?.invoke(event.x, event.y) == true
|
|
672
696
|
horizontalScrollDeferred = overHorizontalScroll
|
|
673
697
|
if (overHorizontalScroll) parent?.requestDisallowInterceptTouchEvent(true)
|
|
674
|
-
swipeTracking = tabs.size > 1 && tabSwipeEnabled && !overHorizontalScroll &&
|
|
698
|
+
swipeTracking = tabs.size > 1 && tabSwipeEnabled && !overHorizontalScroll && tabRow.visibility == View.VISIBLE && event.y < tabRow.top
|
|
675
699
|
swipeIntercepted = false
|
|
676
700
|
swipeDownX = event.x
|
|
677
701
|
swipeDownY = event.y
|
|
@@ -742,7 +766,17 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
742
766
|
buttons.addView(navButton(button))
|
|
743
767
|
}
|
|
744
768
|
val tabBarHidden = options.optBoolean("tabBarHidden", false) || depth > 0 && options.optBoolean("tabBarHiddenWhenPushed", false)
|
|
745
|
-
|
|
769
|
+
tabRow.visibility = if (tabs.isEmpty() || tabBarHidden) View.GONE else View.VISIBLE
|
|
770
|
+
accessorySpec = options.optJSONObject("tabAccessory")?.takeIf {
|
|
771
|
+
val path = it.optString("path")
|
|
772
|
+
path.startsWith("/") && !path.startsWith("//") && it.has("title")
|
|
773
|
+
}
|
|
774
|
+
accessory.visibility = if (accessorySpec == null) View.GONE else View.VISIBLE
|
|
775
|
+
accessorySpec?.let {
|
|
776
|
+
accessory.contentDescription = it.optString("accessibilityLabel").ifBlank { it.optString("title") } + it.optString("badge").takeIf { badge -> badge.isNotBlank() && badge != "null" }?.let { badge -> ", $badge" }.orEmpty()
|
|
777
|
+
accessoryBadge.text = it.optString("badge").takeUnless { badge -> badge == "null" }.orEmpty().take(8)
|
|
778
|
+
accessoryBadge.visibility = if (accessoryBadge.text.isEmpty()) View.GONE else View.VISIBLE
|
|
779
|
+
}
|
|
746
780
|
tabSwipeEnabled = options.optBoolean("tabSwipeEnabled", true)
|
|
747
781
|
refreshColors()
|
|
748
782
|
}
|
|
@@ -801,13 +835,10 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
801
835
|
titleView.setTextColor(text)
|
|
802
836
|
largeTitleView.setTextColor(text)
|
|
803
837
|
backButton.imageTintList = ColorStateList.valueOf(accent)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
divider,
|
|
809
|
-
GradientDrawable().apply { setColor(if (dark) Color.parseColor("#121212") else Color.parseColor("#F9F9F9")) },
|
|
810
|
-
)).apply { setLayerInset(1, 0, maxOf(1, (density / 2).toInt()), 0, 0) }
|
|
838
|
+
val surface = if (dark) Color.parseColor("#1C1C1E") else Color.parseColor("#F9F9F9")
|
|
839
|
+
tabBar.background = GradientDrawable().apply { setColor(surface); cornerRadius = dp(32).toFloat() }
|
|
840
|
+
accessory.background = GradientDrawable().apply { setColor(surface); cornerRadius = dp(32).toFloat() }
|
|
841
|
+
accessorySpec?.let { accessoryIcon.setImageDrawable(CraftNativeIcons.drawable(context, null, it.optString("symbol"), text)) }
|
|
811
842
|
tabViews.forEachIndexed { index, (icon, label) ->
|
|
812
843
|
val color = if (index == selectedTab) accent else muted
|
|
813
844
|
val number = tabIconText[tabs[index].id]
|
|
@@ -256,16 +256,9 @@ private data class CraftNativeLayoutStyle(
|
|
|
256
256
|
aspectRatio = decimal("aspectRatio")?.takeIf { it.isFinite() && it > 0.0 },
|
|
257
257
|
minWidth = length("minWidth"), maxWidth = length("maxWidth"),
|
|
258
258
|
minHeight = length("minHeight"), maxHeight = length("maxHeight"),
|
|
259
|
-
position = raw.opt("position")
|
|
260
|
-
?.takeUnless { it == JSONObject.NULL }
|
|
261
|
-
?.toString()
|
|
262
|
-
?.takeIf { it.isNotBlank() }
|
|
263
|
-
?: "relative",
|
|
259
|
+
position = craftNativeOptionalString(raw.opt("position")) ?: "relative",
|
|
264
260
|
top = number("top"), right = number("right"), bottom = number("bottom"), left = number("left"),
|
|
265
|
-
alignSelf = raw.opt("alignSelf")
|
|
266
|
-
?.takeUnless { it == JSONObject.NULL }
|
|
267
|
-
?.toString()
|
|
268
|
-
?.takeIf { it.isNotBlank() && it != "auto" },
|
|
261
|
+
alignSelf = craftNativeOptionalString(raw.opt("alignSelf"))?.takeUnless { it == "auto" },
|
|
269
262
|
flexGrow = decimal("flexGrow") ?: flex?.coerceAtLeast(0.0) ?: 0.0,
|
|
270
263
|
flexShrink = decimal("flexShrink") ?: flex?.let { if (it != 0.0) 1.0 else 0.0 },
|
|
271
264
|
flexBasis = length("flexBasis")
|
|
@@ -383,11 +376,8 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
383
376
|
fun merge(node: JSONObject): SvgPaint {
|
|
384
377
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
385
378
|
val style = node.optJSONObject("style") ?: JSONObject()
|
|
386
|
-
fun value(name: String): String? =
|
|
387
|
-
|
|
388
|
-
props.has(name) && !props.isNull(name) -> props.optString(name)
|
|
389
|
-
else -> null
|
|
390
|
-
}
|
|
379
|
+
fun value(name: String): String? = craftNativeOptionalString(style.opt(name))
|
|
380
|
+
?: craftNativeOptionalString(props.opt(name))
|
|
391
381
|
fun raw(name: String): Any? = when {
|
|
392
382
|
style.has(name) && !style.isNull(name) -> style.opt(name)
|
|
393
383
|
props.has(name) && !props.isNull(name) -> props.opt(name)
|
|
@@ -486,7 +476,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
486
476
|
for (index in 0 until children.length()) {
|
|
487
477
|
val node = children.optJSONObject(index) ?: continue
|
|
488
478
|
val style = node.optJSONObject("style") ?: JSONObject()
|
|
489
|
-
if (style.
|
|
479
|
+
if (craftNativeOptionalString(style.opt("display")) == "none") continue
|
|
490
480
|
val paint = inherited.merge(node)
|
|
491
481
|
when (node.optString("type")) {
|
|
492
482
|
"G" -> drawNodes(canvas, node.optJSONArray("children") ?: JSONArray(), paint)
|
|
@@ -582,7 +572,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
582
572
|
}
|
|
583
573
|
var x = craftNativeNumber(props.opt("x"))?.toFloat()?.takeIf { it.isFinite() } ?: 0f
|
|
584
574
|
val y = craftNativeNumber(props.opt("y"))?.toFloat()?.takeIf { it.isFinite() } ?: 0f
|
|
585
|
-
when (props.
|
|
575
|
+
when (craftNativeOptionalString(props.opt("textAnchor"))) {
|
|
586
576
|
"middle" -> x -= paint.measureText(text) / 2f
|
|
587
577
|
"end" -> x -= paint.measureText(text)
|
|
588
578
|
}
|
|
@@ -728,12 +718,15 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
728
718
|
val explicitRegion = region(pendingProps.optJSONObject("region"))
|
|
729
719
|
val initialRegion = region(pendingProps.optJSONObject("initialRegion"))
|
|
730
720
|
if (explicitRegion != null) {
|
|
731
|
-
|
|
721
|
+
// A controlled region is an exact camera request. The padding
|
|
722
|
+
// used for fitToBounds is framing chrome, not part of the region
|
|
723
|
+
// contract, and would otherwise zoom every Android screen out.
|
|
724
|
+
moveCamera(explicitRegion, padding = 0)
|
|
732
725
|
initialCameraKey = null
|
|
733
726
|
} else if (initialRegion != null) {
|
|
734
727
|
val key = "region:${pendingProps.optJSONObject("initialRegion")}"
|
|
735
728
|
if (initialCameraKey != key) {
|
|
736
|
-
moveCamera(initialRegion)
|
|
729
|
+
moveCamera(initialRegion, padding = 0)
|
|
737
730
|
initialCameraKey = key
|
|
738
731
|
}
|
|
739
732
|
} else if (craftNativeBoolean(pendingProps.opt("fitToBounds"))) {
|
|
@@ -755,9 +748,9 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
|
|
|
755
748
|
}
|
|
756
749
|
}
|
|
757
750
|
|
|
758
|
-
private fun moveCamera(bounds: LatLngBounds) {
|
|
751
|
+
private fun moveCamera(bounds: LatLngBounds, padding: Int = 24) {
|
|
759
752
|
post {
|
|
760
|
-
if (width > 0 && height > 0) googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds,
|
|
753
|
+
if (width > 0 && height > 0) googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, padding))
|
|
761
754
|
}
|
|
762
755
|
}
|
|
763
756
|
|
|
@@ -1027,14 +1020,15 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1027
1020
|
}
|
|
1028
1021
|
|
|
1029
1022
|
fun configure(style: JSONObject, density: Float) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1023
|
+
val flexDirection = craftNativeOptionalString(style.opt("flexDirection")).orEmpty()
|
|
1024
|
+
orientation = if (flexDirection in setOf("row", "row-reverse")) HORIZONTAL else VERTICAL
|
|
1025
|
+
reverse = flexDirection.endsWith("-reverse")
|
|
1026
|
+
val flexWrap = craftNativeOptionalString(style.opt("flexWrap")).orEmpty()
|
|
1027
|
+
wrap = flexWrap == "wrap" || flexWrap == "wrap-reverse" || craftNativeBoolean(style.opt("flexWrap"))
|
|
1034
1028
|
wrapReverse = flexWrap == "wrap-reverse"
|
|
1035
|
-
alignItems = style.
|
|
1036
|
-
alignContent = style.
|
|
1037
|
-
justifyContent = style.
|
|
1029
|
+
alignItems = craftNativeOptionalString(style.opt("alignItems")) ?: "stretch"
|
|
1030
|
+
alignContent = craftNativeOptionalString(style.opt("alignContent")) ?: "flex-start"
|
|
1031
|
+
justifyContent = craftNativeOptionalString(style.opt("justifyContent")) ?: "flex-start"
|
|
1038
1032
|
fun spacing(name: String, fallback: Float): Float {
|
|
1039
1033
|
val value = craftNativeNumber(style.opt(name)) ?: Double.NaN
|
|
1040
1034
|
val pixels = value * density
|
|
@@ -1044,7 +1038,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1044
1038
|
val gap = spacing("gap", 0f)
|
|
1045
1039
|
rowGap = spacing("rowGap", gap)
|
|
1046
1040
|
columnGap = spacing("columnGap", gap)
|
|
1047
|
-
grid = style.
|
|
1041
|
+
grid = craftNativeOptionalString(style.opt("display")) == "grid"
|
|
1048
1042
|
gridColumns = craftNativeGridColumnCount(
|
|
1049
1043
|
if (style.has("gridTemplateColumns")) style.opt("gridTemplateColumns") else style.opt("gridColumns")
|
|
1050
1044
|
)
|
|
@@ -2095,7 +2089,7 @@ private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(co
|
|
|
2095
2089
|
}
|
|
2096
2090
|
|
|
2097
2091
|
fun configure(props: JSONObject, style: JSONObject, density: Float) {
|
|
2098
|
-
behavior = props.
|
|
2092
|
+
behavior = craftNativeOptionalString(props.opt("behavior"))?.takeIf { it in setOf("height", "position", "padding") } ?: "padding"
|
|
2099
2093
|
val authoredOffset = craftNativeFiniteDouble(props, "keyboardVerticalOffset", 0.0)
|
|
2100
2094
|
val pixelOffset = (authoredOffset * density.toDouble()).takeIf { it.isFinite() } ?: 0.0
|
|
2101
2095
|
keyboardVerticalOffset = pixelOffset.roundToInt()
|
|
@@ -3958,6 +3952,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3958
3952
|
val bars = CraftNativeChrome(this, routes.tabs)
|
|
3959
3953
|
bars.layoutParams = FrameLayout.LayoutParams(-1, -1)
|
|
3960
3954
|
bars.onBack = { onBackPressedDispatcherCompat() }
|
|
3955
|
+
bars.onAccessory = { path -> navigateOpen(path) }
|
|
3961
3956
|
bars.onButton = { id -> visibleScreen?.takeIf { it.active }?.let { send(it, "NAV_BUTTON", JSONObject().put("id", id)) } }
|
|
3962
3957
|
bars.onTab = { index ->
|
|
3963
3958
|
haptic()
|
|
@@ -4945,7 +4940,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4945
4940
|
"Text" -> ((previous as? TextView)?.takeUnless { it is Button || it is EditText } ?: TextView(this)).apply {
|
|
4946
4941
|
includeFontPadding = false
|
|
4947
4942
|
maxLines = craftNativeInt(props.opt("numberOfLines"), 0).takeIf { it > 0 } ?: Int.MAX_VALUE
|
|
4948
|
-
ellipsize = when (props.
|
|
4943
|
+
ellipsize = when (craftNativeOptionalString(props.opt("ellipsizeMode"))) {
|
|
4949
4944
|
"head" -> TextUtils.TruncateAt.START
|
|
4950
4945
|
"middle" -> TextUtils.TruncateAt.MIDDLE
|
|
4951
4946
|
"tail" -> TextUtils.TruncateAt.END
|
|
@@ -5248,7 +5243,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5248
5243
|
// from the previous hidden state when it patches props.
|
|
5249
5244
|
craftNativeBoolean(props.opt("visible"))
|
|
5250
5245
|
} else {
|
|
5251
|
-
style.has("display") && style.
|
|
5246
|
+
style.has("display") && craftNativeOptionalString(style.opt("display")) != "none"
|
|
5252
5247
|
}
|
|
5253
5248
|
val wasVisible = screen.modalVisibility[this]
|
|
5254
5249
|
screen.modalVisibility[this] = visible
|
|
@@ -5480,7 +5475,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5480
5475
|
drawing = node
|
|
5481
5476
|
}
|
|
5482
5477
|
"ScrollView", "SectionList" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
|
|
5483
|
-
val horizontal = craftNativeBoolean(props.opt("horizontal")) || style.
|
|
5478
|
+
val horizontal = craftNativeBoolean(props.opt("horizontal")) || craftNativeOptionalString(style.opt("flexDirection")) in setOf("row", "row-reverse")
|
|
5484
5479
|
setHorizontal(horizontal)
|
|
5485
5480
|
setNativeNestedScrollingEnabled(true)
|
|
5486
5481
|
setScrollIndicators(
|
|
@@ -5488,7 +5483,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5488
5483
|
craftNativeBoolean(props.opt("showsHorizontalScrollIndicator"), true),
|
|
5489
5484
|
)
|
|
5490
5485
|
setScrollEnabled(craftNativeBoolean(props.opt("scrollEnabled"), true))
|
|
5491
|
-
setKeyboardShouldPersistTaps(props.
|
|
5486
|
+
setKeyboardShouldPersistTaps(craftNativeOptionalString(props.opt("keyboardShouldPersistTaps")) ?: "never")
|
|
5492
5487
|
setBounces(craftNativeBoolean(props.opt("bounces"), true))
|
|
5493
5488
|
setAlwaysBounce(
|
|
5494
5489
|
craftNativeBoolean(props.opt("alwaysBounceVertical"), !horizontal),
|
|
@@ -5550,7 +5545,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5550
5545
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject()))
|
|
5551
5546
|
}
|
|
5552
5547
|
},
|
|
5553
|
-
keyboardDismissMode = props.
|
|
5548
|
+
keyboardDismissMode = craftNativeOptionalString(props.opt("keyboardDismissMode")).orEmpty(),
|
|
5554
5549
|
dismissKeyboard = {
|
|
5555
5550
|
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
5556
5551
|
?.hideSoftInputFromWindow(nativeScroll.windowToken, 0)
|
|
@@ -5587,9 +5582,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
5587
5582
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
5588
5583
|
}, craftNativeBoolean(propRefresh.opt("refreshing"), craftNativeBoolean(props.opt("refreshing"))))
|
|
5589
5584
|
setRefreshAppearance(propRefresh, refreshTintFallback(screen))
|
|
5590
|
-
nativeList.setKeyboardShouldPersistTaps(props.
|
|
5585
|
+
nativeList.setKeyboardShouldPersistTaps(craftNativeOptionalString(props.opt("keyboardShouldPersistTaps")) ?: "never")
|
|
5591
5586
|
nativeList.setContentContainerStyle(props.optJSONObject("contentContainerStyle"), resources.displayMetrics.density)
|
|
5592
|
-
nativeList.setKeyboardDismissMode(props.
|
|
5587
|
+
nativeList.setKeyboardDismissMode(craftNativeOptionalString(props.opt("keyboardDismissMode")).orEmpty()) {
|
|
5593
5588
|
(getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
|
|
5594
5589
|
?.hideSoftInputFromWindow(windowToken, 0)
|
|
5595
5590
|
}
|
|
@@ -5598,6 +5593,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5598
5593
|
"View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
5599
5594
|
configureContainer(this, style)
|
|
5600
5595
|
if (type == "SafeAreaView") setSafeAreaEdges(props.optJSONArray("edges"))
|
|
5596
|
+
else setWindowSafeInsets(0, 0, 0, 0)
|
|
5601
5597
|
reconcileChildren(screen, this, children, identity, next, style, previousControls)
|
|
5602
5598
|
}
|
|
5603
5599
|
"KeyboardAvoidingView" -> (previous as? CraftNativeKeyboardAvoidingView ?: CraftNativeKeyboardAvoidingView(this)).apply {
|
|
@@ -5744,7 +5740,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5744
5740
|
if (type == "ActivityIndicator") {
|
|
5745
5741
|
val animating = craftNativeBoolean(props.opt("animating"), true)
|
|
5746
5742
|
val hidesWhenStopped = craftNativeBoolean(props.opt("hidesWhenStopped"), true)
|
|
5747
|
-
control.visibility = if (style.
|
|
5743
|
+
control.visibility = if (craftNativeOptionalString(style.opt("display")) == "none" || (!animating && hidesWhenStopped)) {
|
|
5748
5744
|
View.GONE
|
|
5749
5745
|
} else {
|
|
5750
5746
|
View.VISIBLE
|
|
@@ -5865,7 +5861,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5865
5861
|
layout.setChildStyle(childView, child.optJSONObject("style") ?: JSONObject(), resources.displayMetrics.density)
|
|
5866
5862
|
}
|
|
5867
5863
|
}
|
|
5868
|
-
if (style.
|
|
5864
|
+
if (craftNativeOptionalString(style.opt("flexDirection"))?.endsWith("-reverse") == true) desired.reverse()
|
|
5869
5865
|
val desiredSet = desired.toSet()
|
|
5870
5866
|
for ((index, childView) in desired.withIndex()) {
|
|
5871
5867
|
if (layout.getChildAt(index) !== childView) {
|
|
@@ -5992,7 +5988,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5992
5988
|
}
|
|
5993
5989
|
applyTransform(view, style.optJSONArray("transform"))
|
|
5994
5990
|
if (view !is CraftNativeModalView) {
|
|
5995
|
-
view.visibility = if (style.
|
|
5991
|
+
view.visibility = if (craftNativeOptionalString(style.opt("display")) == "none") View.GONE else View.VISIBLE
|
|
5996
5992
|
}
|
|
5997
5993
|
view.elevation = dp(craftNativeNonNegativeDouble(style, "elevation", 0.0)).toFloat()
|
|
5998
5994
|
view.z = craftNativeFiniteFloat(style, "zIndex", 0f)
|
|
@@ -6001,13 +5997,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
6001
5997
|
// background paints over the decoded picture (only rounded corners
|
|
6002
5998
|
// showed it). Keep the outline/borders, with a transparent fill.
|
|
6003
5999
|
val backgroundColor = if (view is CraftNativeVideoView) Color.TRANSPARENT
|
|
6004
|
-
else color(style.
|
|
6000
|
+
else color(craftNativeOptionalString(style.opt("backgroundColor")).orEmpty(), Color.TRANSPARENT)
|
|
6005
6001
|
val border = craftNativeNonNegativeDouble(style, "borderWidth", 0.0)
|
|
6006
6002
|
val radius = craftNativeNonNegativeDouble(style, "borderRadius", 0.0)
|
|
6007
|
-
val gradientDirection = style.
|
|
6008
|
-
val gradientFrom = style.
|
|
6009
|
-
val gradientVia = style.
|
|
6010
|
-
val gradientTo = style.
|
|
6003
|
+
val gradientDirection = craftNativeOptionalString(style.opt("gradientDirection"))
|
|
6004
|
+
val gradientFrom = craftNativeOptionalString(style.opt("gradientFrom"))
|
|
6005
|
+
val gradientVia = craftNativeOptionalString(style.opt("gradientVia"))
|
|
6006
|
+
val gradientTo = craftNativeOptionalString(style.opt("gradientTo"))
|
|
6011
6007
|
val gradientColors = if (gradientDirection != null && gradientFrom != null && gradientTo != null) {
|
|
6012
6008
|
listOfNotNull(
|
|
6013
6009
|
color(gradientFrom, Color.TRANSPARENT),
|
|
@@ -6025,7 +6021,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6025
6021
|
listOf(radius, radius)
|
|
6026
6022
|
}.toFloatArray()
|
|
6027
6023
|
val sideColors = listOf("borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor")
|
|
6028
|
-
.map { color(style.
|
|
6024
|
+
.map { color(craftNativeOptionalString(style.opt(it)) ?: craftNativeOptionalString(style.opt("borderColor")).orEmpty(), Color.TRANSPARENT) }
|
|
6029
6025
|
.toIntArray()
|
|
6030
6026
|
val defaultBackground = if (defaultBackgrounds.containsKey(view)) {
|
|
6031
6027
|
defaultBackgrounds[view]
|
|
@@ -6039,7 +6035,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6039
6035
|
cornerRadii,
|
|
6040
6036
|
sideWidths,
|
|
6041
6037
|
sideColors,
|
|
6042
|
-
style.
|
|
6038
|
+
craftNativeOptionalString(style.opt("borderStyle"))?.takeIf { it == "dashed" || it == "dotted" },
|
|
6043
6039
|
gradientDirection,
|
|
6044
6040
|
gradientColors,
|
|
6045
6041
|
)
|
|
@@ -6051,8 +6047,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
6051
6047
|
} else setColor(backgroundColor)
|
|
6052
6048
|
if (hasCornerRadii) setCornerRadii(cornerRadii) else cornerRadius = dp(radius).toFloat()
|
|
6053
6049
|
if (border > 0) {
|
|
6054
|
-
val borderColor = color(style.
|
|
6055
|
-
when (style.
|
|
6050
|
+
val borderColor = color(craftNativeOptionalString(style.opt("borderColor")).orEmpty(), Color.TRANSPARENT)
|
|
6051
|
+
when (craftNativeOptionalString(style.opt("borderStyle"))) {
|
|
6056
6052
|
"dashed" -> setStroke(dp(border), borderColor, dp(border * 3).toFloat(), dp(border * 2).toFloat())
|
|
6057
6053
|
"dotted" -> setStroke(dp(border), borderColor, dp(border).toFloat(), dp(border * 1.5).toFloat())
|
|
6058
6054
|
else -> setStroke(dp(border), borderColor)
|
|
@@ -6065,13 +6061,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
6065
6061
|
// and the web.
|
|
6066
6062
|
null
|
|
6067
6063
|
} else defaultBackground
|
|
6068
|
-
|
|
6069
|
-
|
|
6064
|
+
val overflow = craftNativeOptionalString(style.opt("overflow"))
|
|
6065
|
+
view.clipToOutline = overflow == "hidden"
|
|
6066
|
+
if (view is ViewGroup) view.clipChildren = overflow == "hidden"
|
|
6070
6067
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
|
6071
6068
|
(style.has("shadowColor") || style.has("shadowOpacity") || style.has("shadowRadius"))
|
|
6072
6069
|
) {
|
|
6073
6070
|
val shadowOpacity = craftNativeFiniteFloat(style, "shadowOpacity", 1f).coerceIn(0f, 1f)
|
|
6074
|
-
val shadow = color(style.
|
|
6071
|
+
val shadow = color(craftNativeOptionalString(style.opt("shadowColor")).orEmpty(), Color.BLACK)
|
|
6075
6072
|
val alphaShadow = Color.argb((Color.alpha(shadow) * shadowOpacity).roundToInt(), Color.red(shadow), Color.green(shadow), Color.blue(shadow))
|
|
6076
6073
|
view.outlineAmbientShadowColor = alphaShadow
|
|
6077
6074
|
view.outlineSpotShadowColor = alphaShadow
|
|
@@ -6169,7 +6166,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6169
6166
|
}
|
|
6170
6167
|
|
|
6171
6168
|
private fun configureText(view: TextView, value: String, style: JSONObject) {
|
|
6172
|
-
view.text = when (style.
|
|
6169
|
+
view.text = when (craftNativeOptionalString(style.opt("textTransform"))) {
|
|
6173
6170
|
"uppercase" -> value.uppercase()
|
|
6174
6171
|
"lowercase" -> value.lowercase()
|
|
6175
6172
|
"capitalize" -> value.split(' ').joinToString(" ") { word -> word.replaceFirstChar(Char::uppercase) }
|
|
@@ -6279,21 +6276,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
6279
6276
|
}
|
|
6280
6277
|
|
|
6281
6278
|
private fun configureTextStyle(view: TextView, style: JSONObject) {
|
|
6282
|
-
view.setTextColor(color(style.
|
|
6279
|
+
view.setTextColor(color(craftNativeOptionalString(style.opt("color")).orEmpty(), defaultTextColor))
|
|
6283
6280
|
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, craftNativeFiniteFloat(style, "fontSize", 16f))
|
|
6284
|
-
val fontWeight = style.opt("fontWeight")
|
|
6281
|
+
val fontWeight = craftNativeFontWeight(style.opt("fontWeight"))
|
|
6285
6282
|
val weight = when (fontWeight) {
|
|
6286
6283
|
"bold" -> 700
|
|
6287
6284
|
"100", "200", "300", "400", "500", "600", "700", "800", "900" -> style.optInt("fontWeight")
|
|
6288
6285
|
else -> 400
|
|
6289
6286
|
}
|
|
6290
|
-
val italic = style.
|
|
6291
|
-
val family = style.
|
|
6287
|
+
val italic = craftNativeOptionalString(style.opt("fontStyle")) == "italic"
|
|
6288
|
+
val family = craftNativeOptionalString(style.opt("fontFamily"))?.takeIf { it.isNotBlank() }
|
|
6292
6289
|
view.typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
6293
6290
|
Typeface.create(Typeface.create(family, Typeface.NORMAL), weight, italic)
|
|
6294
6291
|
} else Typeface.create(family, (if (weight >= 600) Typeface.BOLD else Typeface.NORMAL) or
|
|
6295
6292
|
(if (italic) Typeface.ITALIC else Typeface.NORMAL))
|
|
6296
|
-
|
|
6293
|
+
val textAlign = craftNativeOptionalString(style.opt("textAlign"))
|
|
6294
|
+
view.textAlignment = when (textAlign) {
|
|
6297
6295
|
"left" -> View.TEXT_ALIGNMENT_TEXT_START
|
|
6298
6296
|
"right" -> View.TEXT_ALIGNMENT_TEXT_END
|
|
6299
6297
|
"center" -> View.TEXT_ALIGNMENT_CENTER
|
|
@@ -6301,7 +6299,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6301
6299
|
else -> View.TEXT_ALIGNMENT_INHERIT
|
|
6302
6300
|
}
|
|
6303
6301
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
6304
|
-
view.justificationMode = if (
|
|
6302
|
+
view.justificationMode = if (textAlign == "justify") {
|
|
6305
6303
|
Layout.JUSTIFICATION_MODE_INTER_WORD
|
|
6306
6304
|
} else {
|
|
6307
6305
|
Layout.JUSTIFICATION_MODE_NONE
|
|
@@ -6315,7 +6313,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6315
6313
|
craftNativeFiniteDouble(style, "lineHeight", Double.NaN).takeIf { it.isFinite() }?.let { dp(it) - view.textSize } ?: 0f,
|
|
6316
6314
|
1f
|
|
6317
6315
|
)
|
|
6318
|
-
val shadowColor = style.
|
|
6316
|
+
val shadowColor = craftNativeOptionalString(style.opt("textShadowColor"))?.takeIf { it.isNotBlank() }
|
|
6319
6317
|
if (shadowColor == null) {
|
|
6320
6318
|
view.paint.clearShadowLayer()
|
|
6321
6319
|
} else {
|
|
@@ -6332,16 +6330,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
6332
6330
|
}
|
|
6333
6331
|
|
|
6334
6332
|
private fun applyTextDecoration(view: TextView, style: JSONObject) {
|
|
6335
|
-
val line = style.
|
|
6333
|
+
val line = craftNativeOptionalString(style.opt("textDecorationLine")).orEmpty()
|
|
6336
6334
|
if (view is EditText) {
|
|
6337
6335
|
// A field's own text, spanned in place: replacing it would move
|
|
6338
6336
|
// the caret to the start and tell the screen the text changed.
|
|
6339
6337
|
val editable = view.text ?: return
|
|
6340
6338
|
editable.getSpans(0, editable.length, CraftTextDecorationSpan::class.java).forEach(editable::removeSpan)
|
|
6341
6339
|
if (line.isBlank() || line == "none" || editable.isEmpty()) return
|
|
6342
|
-
val fieldColor = style.
|
|
6340
|
+
val fieldColor = craftNativeOptionalString(style.opt("textDecorationColor"))?.takeIf { it.isNotBlank() }?.let { color(it, Color.TRANSPARENT) }
|
|
6343
6341
|
editable.setSpan(
|
|
6344
|
-
CraftTextDecorationSpan(line, style.
|
|
6342
|
+
CraftTextDecorationSpan(line, craftNativeOptionalString(style.opt("textDecorationStyle")).orEmpty().ifBlank { "solid" }, fieldColor),
|
|
6345
6343
|
0,
|
|
6346
6344
|
editable.length,
|
|
6347
6345
|
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
|
|
@@ -6353,14 +6351,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
6353
6351
|
if (view.text is Spannable) view.text = text
|
|
6354
6352
|
return
|
|
6355
6353
|
}
|
|
6356
|
-
val decorationColor = style.
|
|
6354
|
+
val decorationColor = craftNativeOptionalString(style.opt("textDecorationColor")).orEmpty()
|
|
6357
6355
|
.takeIf { it.isNotBlank() }
|
|
6358
6356
|
?.let { color(it, Color.TRANSPARENT) }
|
|
6359
6357
|
val decorated = SpannableString(text)
|
|
6360
6358
|
decorated.setSpan(
|
|
6361
6359
|
CraftTextDecorationSpan(
|
|
6362
6360
|
line,
|
|
6363
|
-
style.
|
|
6361
|
+
craftNativeOptionalString(style.opt("textDecorationStyle")).orEmpty().ifBlank { "solid" },
|
|
6364
6362
|
decorationColor,
|
|
6365
6363
|
),
|
|
6366
6364
|
0,
|
|
@@ -6568,8 +6566,16 @@ class MainActivity : AppCompatActivity() {
|
|
|
6568
6566
|
info.roleDescription = role.takeIf { it !in setOf("view", "text", "textinput", "scrollview") }
|
|
6569
6567
|
info.hintText = host.tooltipText
|
|
6570
6568
|
info.isEnabled = host.isEnabled && !disabled
|
|
6571
|
-
|
|
6572
|
-
|
|
6569
|
+
// Keep the native widget state when no accessibilityState was
|
|
6570
|
+
// authored. Overwriting it with the strict false fallback
|
|
6571
|
+
// made a checked Switch/CheckBox announce as unchecked unless
|
|
6572
|
+
// the screen redundantly mirrored its value into metadata.
|
|
6573
|
+
if (state?.has("selected") == true) {
|
|
6574
|
+
info.isSelected = craftNativeBoolean(state.opt("selected"))
|
|
6575
|
+
}
|
|
6576
|
+
if (state?.has("checked") == true) {
|
|
6577
|
+
info.isChecked = craftNativeBoolean(state.opt("checked"))
|
|
6578
|
+
}
|
|
6573
6579
|
}
|
|
6574
6580
|
})
|
|
6575
6581
|
ViewCompat.setImportantForAccessibility(
|
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.210";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
|
@@ -245,7 +245,7 @@ export interface TextStyleProps {
|
|
|
245
245
|
/** Font style */
|
|
246
246
|
fontStyle?: 'normal' | 'italic';
|
|
247
247
|
/** Font weight */
|
|
248
|
-
fontWeight?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
|
|
248
|
+
fontWeight?: number | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
|
|
249
249
|
/** Letter spacing */
|
|
250
250
|
letterSpacing?: number;
|
|
251
251
|
/** Line height */
|
|
@@ -287,6 +287,8 @@ export interface ImageStyleProps {
|
|
|
287
287
|
* Combined Image style type.
|
|
288
288
|
*/
|
|
289
289
|
export type ImageStyle = ViewStyle & ImageStyleProps;
|
|
290
|
+
/** A native style may be nested or conditionally omitted in any component's `style` prop. */
|
|
291
|
+
export type StyleValue<T> = T | StyleValue<T>[] | null | false | undefined;
|
|
290
292
|
/**
|
|
291
293
|
* Base props for all components.
|
|
292
294
|
*/
|
|
@@ -320,7 +322,7 @@ export interface AccessibilityState {
|
|
|
320
322
|
*/
|
|
321
323
|
export interface ViewProps extends BaseProps {
|
|
322
324
|
/** Style object or array */
|
|
323
|
-
style?: ViewStyle
|
|
325
|
+
style?: StyleValue<ViewStyle>;
|
|
324
326
|
/** Press handler */
|
|
325
327
|
onPress?: () => void;
|
|
326
328
|
/** DOM-compatible press alias accepted by native hosts. */
|
|
@@ -356,19 +358,23 @@ export interface RefreshControlProps extends BaseProps {
|
|
|
356
358
|
*/
|
|
357
359
|
export interface TextProps extends BaseProps {
|
|
358
360
|
/** Style object or array */
|
|
359
|
-
style?: TextStyle
|
|
361
|
+
style?: StyleValue<TextStyle>;
|
|
360
362
|
/** Number of lines before truncation */
|
|
361
363
|
numberOfLines?: number;
|
|
362
364
|
/** Ellipsize mode */
|
|
363
365
|
ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
|
|
364
366
|
/** Press handler */
|
|
365
367
|
onPress?: () => void;
|
|
368
|
+
/** DOM-compatible press alias accepted by native hosts. */
|
|
369
|
+
onClick?: () => void;
|
|
366
370
|
/** Long press handler */
|
|
367
371
|
onLongPress?: () => void;
|
|
368
372
|
}
|
|
369
373
|
/** Native button/link action props. */
|
|
370
374
|
export interface ButtonProps extends BaseProps {
|
|
371
375
|
title?: string;
|
|
376
|
+
/** Text and layout styling applied by the native button hosts. */
|
|
377
|
+
style?: StyleValue<TextStyle>;
|
|
372
378
|
onPress?: () => void;
|
|
373
379
|
onClick?: () => void;
|
|
374
380
|
disabled?: boolean;
|
|
@@ -393,10 +399,17 @@ export interface TouchableHighlightProps extends ViewProps {
|
|
|
393
399
|
/**
|
|
394
400
|
* Image source type.
|
|
395
401
|
*/
|
|
396
|
-
export type ImageSource = {
|
|
402
|
+
export type ImageSource = string | {
|
|
397
403
|
uri: string;
|
|
398
404
|
headers?: Record<string, string>;
|
|
399
405
|
};
|
|
406
|
+
export interface ImageErrorEvent {
|
|
407
|
+
nativeEvent: {
|
|
408
|
+
error: {
|
|
409
|
+
message: string;
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
}
|
|
400
413
|
/**
|
|
401
414
|
* Image component props.
|
|
402
415
|
*/
|
|
@@ -404,7 +417,7 @@ export interface ImageProps extends BaseProps {
|
|
|
404
417
|
/** Image source */
|
|
405
418
|
source: ImageSource;
|
|
406
419
|
/** Style object or array */
|
|
407
|
-
style?: ImageStyle
|
|
420
|
+
style?: StyleValue<ImageStyle>;
|
|
408
421
|
/** Resize mode */
|
|
409
422
|
resizeMode?: 'cover' | 'contain' | 'stretch' | 'center';
|
|
410
423
|
/** Load start handler */
|
|
@@ -413,19 +426,21 @@ export interface ImageProps extends BaseProps {
|
|
|
413
426
|
onLoad?: () => void;
|
|
414
427
|
/** Load end handler */
|
|
415
428
|
onLoadEnd?: () => void;
|
|
416
|
-
/** Error handler */
|
|
417
|
-
onError?: (
|
|
429
|
+
/** Error handler; native failures are exposed as nativeEvent.error.message. */
|
|
430
|
+
onError?: (event: ImageErrorEvent) => void;
|
|
418
431
|
/** Default source while loading */
|
|
419
432
|
defaultSource?: ImageSource;
|
|
420
433
|
/** Alt text for accessibility */
|
|
421
434
|
alt?: string;
|
|
422
435
|
}
|
|
423
436
|
export interface IconProps extends BaseProps {
|
|
437
|
+
style?: StyleValue<TextStyle>;
|
|
424
438
|
name?: string;
|
|
425
439
|
symbol?: string;
|
|
426
440
|
color?: string;
|
|
427
441
|
}
|
|
428
442
|
export interface VideoProps extends BaseProps {
|
|
443
|
+
style?: StyleValue<ViewStyle>;
|
|
429
444
|
source?: string | {
|
|
430
445
|
uri: string;
|
|
431
446
|
};
|
|
@@ -472,6 +487,7 @@ export interface MapRegion {
|
|
|
472
487
|
};
|
|
473
488
|
}
|
|
474
489
|
export interface MapProps extends BaseProps {
|
|
490
|
+
style?: StyleValue<ViewStyle>;
|
|
475
491
|
region?: MapRegion;
|
|
476
492
|
/** Initial camera region; applied once, then controlled by subsequent props. */
|
|
477
493
|
initialRegion?: MapRegion;
|
|
@@ -512,7 +528,7 @@ export interface ImageBackgroundProps extends ImageProps {
|
|
|
512
528
|
}
|
|
513
529
|
/** Shared paint and accessibility props for DOM-free native SVG nodes. */
|
|
514
530
|
export interface SvgPaintProps extends BaseProps {
|
|
515
|
-
style?: ViewStyle
|
|
531
|
+
style?: StyleValue<ViewStyle>;
|
|
516
532
|
fill?: string;
|
|
517
533
|
stroke?: string;
|
|
518
534
|
strokeWidth?: number;
|
|
@@ -629,7 +645,7 @@ export interface PressableProps extends ViewProps {
|
|
|
629
645
|
*/
|
|
630
646
|
export interface TextInputProps extends BaseProps {
|
|
631
647
|
/** Style object or array */
|
|
632
|
-
style?: TextStyle
|
|
648
|
+
style?: StyleValue<TextStyle>;
|
|
633
649
|
/** Current value */
|
|
634
650
|
value?: string;
|
|
635
651
|
/** Default value */
|
|
@@ -680,6 +696,7 @@ export interface TextInputProps extends BaseProps {
|
|
|
680
696
|
export type KeyboardType = 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'number-pad' | 'decimal-pad' | 'url' | 'web-search' | 'visible-password';
|
|
681
697
|
export type ReturnKeyType = 'done' | 'go' | 'next' | 'search' | 'send' | 'join' | 'route' | 'continue' | 'emergency-call' | 'default';
|
|
682
698
|
export interface SwitchProps extends BaseProps {
|
|
699
|
+
style?: StyleValue<ViewStyle>;
|
|
683
700
|
/** Controlled boolean state; malformed native bridge values stay off. */
|
|
684
701
|
value?: boolean;
|
|
685
702
|
onValueChange?: (value: boolean) => void;
|
|
@@ -693,6 +710,7 @@ export interface SwitchProps extends BaseProps {
|
|
|
693
710
|
ios_backgroundColor?: string;
|
|
694
711
|
}
|
|
695
712
|
export interface CheckboxProps extends BaseProps {
|
|
713
|
+
style?: StyleValue<ViewStyle>;
|
|
696
714
|
/** Controlled boolean state; malformed native bridge values stay off. */
|
|
697
715
|
value?: boolean;
|
|
698
716
|
checked?: boolean;
|
|
@@ -704,6 +722,7 @@ export interface CheckboxProps extends BaseProps {
|
|
|
704
722
|
color?: string;
|
|
705
723
|
}
|
|
706
724
|
export interface SliderProps extends BaseProps {
|
|
725
|
+
style?: StyleValue<ViewStyle>;
|
|
707
726
|
value?: number;
|
|
708
727
|
minimumValue?: number;
|
|
709
728
|
maximumValue?: number;
|
|
@@ -722,6 +741,7 @@ export interface PickerOption {
|
|
|
722
741
|
value: string | number;
|
|
723
742
|
}
|
|
724
743
|
export interface PickerProps extends BaseProps {
|
|
744
|
+
style?: StyleValue<ViewStyle>;
|
|
725
745
|
/** Typed options; child options remain supported when omitted. */
|
|
726
746
|
items?: PickerOption[];
|
|
727
747
|
selectedValue?: string | number;
|
|
@@ -735,7 +755,8 @@ export interface DatePickerChange {
|
|
|
735
755
|
timestamp: number;
|
|
736
756
|
}
|
|
737
757
|
export interface DatePickerProps extends BaseProps {
|
|
738
|
-
|
|
758
|
+
style?: StyleValue<ViewStyle>;
|
|
759
|
+
mode?: 'date' | 'time' | 'datetime' | 'datetime-local' | 'countdown';
|
|
739
760
|
value?: string | number;
|
|
740
761
|
minimumDate?: string | number;
|
|
741
762
|
maximumDate?: string | number;
|
|
@@ -745,6 +766,7 @@ export interface DatePickerProps extends BaseProps {
|
|
|
745
766
|
disabled?: boolean;
|
|
746
767
|
}
|
|
747
768
|
export interface ActivityIndicatorProps extends BaseProps {
|
|
769
|
+
style?: StyleValue<ViewStyle>;
|
|
748
770
|
color?: string;
|
|
749
771
|
animating?: boolean;
|
|
750
772
|
hidesWhenStopped?: boolean;
|
|
@@ -782,6 +804,7 @@ export interface SectionListSection<T> {
|
|
|
782
804
|
export interface SectionListProps<T> extends ScrollViewProps {
|
|
783
805
|
}
|
|
784
806
|
export interface ModalProps extends BaseProps {
|
|
807
|
+
style?: StyleValue<ViewStyle>;
|
|
785
808
|
visible?: boolean;
|
|
786
809
|
transparent?: boolean;
|
|
787
810
|
animationType?: 'none' | 'fade' | 'slide';
|
|
@@ -893,9 +916,9 @@ export declare const StyleSheet: {
|
|
|
893
916
|
*/
|
|
894
917
|
create<T extends Record<string, ViewStyle | TextStyle | ImageStyle>>(styles: T): T;
|
|
895
918
|
/**
|
|
896
|
-
* Flatten
|
|
919
|
+
* Flatten nested style arrays and ignore falsy conditional entries.
|
|
897
920
|
*/
|
|
898
|
-
flatten<T>(style: T
|
|
921
|
+
flatten<T>(style: StyleValue<T>): T | undefined;
|
|
899
922
|
/**
|
|
900
923
|
* Get the absolute fill style (full parent).
|
|
901
924
|
*/
|
package/dist/index.cjs
CHANGED
|
@@ -309,7 +309,7 @@ function craftBinaryNotFoundMessage(triedPath) {
|
|
|
309
309
|
`);
|
|
310
310
|
}
|
|
311
311
|
// package.json
|
|
312
|
-
var version = "0.0.
|
|
312
|
+
var version = "0.0.210";
|
|
313
313
|
|
|
314
314
|
// src/sdk-version.ts
|
|
315
315
|
var SDK_VERSION = version;
|
|
@@ -8625,10 +8625,19 @@ var StyleSheet = {
|
|
|
8625
8625
|
flatten(style) {
|
|
8626
8626
|
if (!style)
|
|
8627
8627
|
return;
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8628
|
+
const flattened = {};
|
|
8629
|
+
const merge = (value) => {
|
|
8630
|
+
if (!value)
|
|
8631
|
+
return;
|
|
8632
|
+
if (Array.isArray(value)) {
|
|
8633
|
+
value.forEach(merge);
|
|
8634
|
+
return;
|
|
8635
|
+
}
|
|
8636
|
+
if (typeof value === "object")
|
|
8637
|
+
Object.assign(flattened, value);
|
|
8638
|
+
};
|
|
8639
|
+
merge(style);
|
|
8640
|
+
return flattened;
|
|
8632
8641
|
},
|
|
8633
8642
|
absoluteFill: {
|
|
8634
8643
|
position: "absolute",
|
package/dist/index.js
CHANGED
|
@@ -57,7 +57,7 @@ function craftBinaryNotFoundMessage(triedPath) {
|
|
|
57
57
|
`);
|
|
58
58
|
}
|
|
59
59
|
// package.json
|
|
60
|
-
var version = "0.0.
|
|
60
|
+
var version = "0.0.210";
|
|
61
61
|
|
|
62
62
|
// src/sdk-version.ts
|
|
63
63
|
var SDK_VERSION = version;
|
|
@@ -8384,10 +8384,19 @@ var StyleSheet = {
|
|
|
8384
8384
|
flatten(style) {
|
|
8385
8385
|
if (!style)
|
|
8386
8386
|
return;
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8387
|
+
const flattened = {};
|
|
8388
|
+
const merge = (value) => {
|
|
8389
|
+
if (!value)
|
|
8390
|
+
return;
|
|
8391
|
+
if (Array.isArray(value)) {
|
|
8392
|
+
value.forEach(merge);
|
|
8393
|
+
return;
|
|
8394
|
+
}
|
|
8395
|
+
if (typeof value === "object")
|
|
8396
|
+
Object.assign(flattened, value);
|
|
8397
|
+
};
|
|
8398
|
+
merge(style);
|
|
8399
|
+
return flattened;
|
|
8391
8400
|
},
|
|
8392
8401
|
absoluteFill: {
|
|
8393
8402
|
position: "absolute",
|
|
@@ -621,6 +621,7 @@ final class CraftChrome: ObservableObject {
|
|
|
621
621
|
}
|
|
622
622
|
|
|
623
623
|
@Published var tabs: [Tab] = []
|
|
624
|
+
@Published var tabAccessory: Tab?
|
|
624
625
|
@Published var selected: String?
|
|
625
626
|
@Published var tabBarVisible = false
|
|
626
627
|
@Published var splashVisible = true
|
|
@@ -688,6 +689,16 @@ final class CraftChrome: ObservableObject {
|
|
|
688
689
|
tabs[index].badge = (item["badge"] as? String).flatMap { $0.isEmpty ? nil : String($0.prefix(8)) }
|
|
689
690
|
}
|
|
690
691
|
}
|
|
692
|
+
/// A destination beside the tab pill, outside the tab stacks and swipe order.
|
|
693
|
+
func updateTabAccessory(_ item: [String: Any]?) {
|
|
694
|
+
guard let item, let path = item["path"] as? String,
|
|
695
|
+
path.hasPrefix("/"), !path.hasPrefix("//"),
|
|
696
|
+
let title = item["title"] as? String else { tabAccessory = nil; return }
|
|
697
|
+
tabAccessory = Tab(id: path, title: title, symbol: item["symbol"] as? String ?? "message",
|
|
698
|
+
badge: (item["badge"] as? String).flatMap { $0.isEmpty ? nil : String($0.prefix(8)) },
|
|
699
|
+
accessibilityLabel: item["accessibilityLabel"] as? String)
|
|
700
|
+
}
|
|
701
|
+
|
|
691
702
|
func setHiddenByScreen(_ hidden: Bool) {
|
|
692
703
|
guard hiddenByScreen != hidden else { return }
|
|
693
704
|
hiddenByScreen = hidden
|
|
@@ -913,7 +924,33 @@ struct CraftTabBarView: View {
|
|
|
913
924
|
|
|
914
925
|
var body: some View {
|
|
915
926
|
if chrome.tabBarVisible && !chrome.tabs.isEmpty {
|
|
916
|
-
|
|
927
|
+
HStack(spacing: 8) {
|
|
928
|
+
bar
|
|
929
|
+
if let accessory = chrome.tabAccessory {
|
|
930
|
+
Button { CraftHybrid.shared.open(accessory.id) } label: {
|
|
931
|
+
surface {
|
|
932
|
+
Image(systemName: accessory.symbol)
|
|
933
|
+
.font(.system(size: iconSize))
|
|
934
|
+
.foregroundStyle(Color.primary)
|
|
935
|
+
.frame(width: 62, height: 62)
|
|
936
|
+
}
|
|
937
|
+
.overlay(alignment: .topTrailing) {
|
|
938
|
+
if let badge = accessory.badge {
|
|
939
|
+
Text(badge)
|
|
940
|
+
.font(.system(size: badgeSize, weight: .bold))
|
|
941
|
+
.foregroundStyle(.white)
|
|
942
|
+
.padding(.horizontal, 4)
|
|
943
|
+
.frame(minWidth: 18, minHeight: 18)
|
|
944
|
+
.background(Capsule().fill(Color.red))
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
.buttonStyle(.plain)
|
|
949
|
+
.accessibilityLabel(accessory.accessibilityLabel ?? accessory.title)
|
|
950
|
+
.accessibilityValue(accessory.badge ?? "")
|
|
951
|
+
.accessibilityIdentifier("craft-tab-accessory")
|
|
952
|
+
}
|
|
953
|
+
}
|
|
917
954
|
.padding(.horizontal, 16)
|
|
918
955
|
.padding(.bottom, 4)
|
|
919
956
|
.background(GeometryReader { proxy in
|
|
@@ -932,24 +969,27 @@ struct CraftTabBarView: View {
|
|
|
932
969
|
chrome.layoutChanged(max(0, screen - proxy.frame(in: .global).minY))
|
|
933
970
|
}
|
|
934
971
|
|
|
935
|
-
|
|
972
|
+
private var bar: some View {
|
|
973
|
+
surface { items.padding(4) }
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/// Both the tab capsule and its circular accessory use the same material,
|
|
977
|
+
/// outline and shadow. A square capsule forms the accessory's circle.
|
|
978
|
+
@ViewBuilder private func surface<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
|
936
979
|
#if compiler(>=6.2)
|
|
937
980
|
if #available(iOS 26.0, *) {
|
|
938
981
|
GlassEffectContainer {
|
|
939
|
-
|
|
940
|
-
.padding(4)
|
|
982
|
+
content()
|
|
941
983
|
.glassEffect(.regular.interactive(), in: .capsule)
|
|
942
984
|
}
|
|
943
985
|
} else {
|
|
944
|
-
|
|
945
|
-
.padding(4)
|
|
986
|
+
content()
|
|
946
987
|
.background(.ultraThinMaterial, in: Capsule())
|
|
947
988
|
.overlay(Capsule().strokeBorder(Color.white.opacity(0.35), lineWidth: 0.5))
|
|
948
989
|
.shadow(color: .black.opacity(0.12), radius: 18, y: 6)
|
|
949
990
|
}
|
|
950
991
|
#else
|
|
951
|
-
|
|
952
|
-
.padding(4)
|
|
992
|
+
content()
|
|
953
993
|
.background(.ultraThinMaterial, in: Capsule())
|
|
954
994
|
.overlay(Capsule().strokeBorder(Color.white.opacity(0.35), lineWidth: 0.5))
|
|
955
995
|
.shadow(color: .black.opacity(0.12), radius: 18, y: 6)
|
|
@@ -3959,11 +3959,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3959
3959
|
// Each screen says whether it wants the tab bar, so one that hid it
|
|
3960
3960
|
// never leaves it hidden for the next.
|
|
3961
3961
|
CraftChrome.shared.setHiddenByScreen(tabBarHidden)
|
|
3962
|
+
CraftChrome.shared.updateTabAccessory(tabAccessory)
|
|
3962
3963
|
refreshTraitDefaults()
|
|
3963
3964
|
}
|
|
3964
3965
|
|
|
3965
3966
|
override func viewWillDisappear(_ animated: Bool) {
|
|
3966
3967
|
super.viewWillDisappear(animated)
|
|
3968
|
+
// The accessory belongs to the visible native screen. Clear the
|
|
3969
|
+
// shared shell state before a hybrid/web route appears, otherwise a
|
|
3970
|
+
// destination from the outgoing screen remains tappable on top of it.
|
|
3971
|
+
CraftChrome.shared.updateTabAccessory(nil)
|
|
3967
3972
|
// Popped (or its stack dismissed) while the bar was away: it comes
|
|
3968
3973
|
// back for whatever shows next, a web page included.
|
|
3969
3974
|
if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
|
|
@@ -4146,6 +4151,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4146
4151
|
let appVersion = Self.jsonString(info?["CFBundleShortVersionString"] as? String ?? "")
|
|
4147
4152
|
let appBuild = Self.jsonString(info?["CFBundleVersion"] as? String ?? "")
|
|
4148
4153
|
jsContext.evaluateScript("""
|
|
4154
|
+
// JavaScriptCore has no DOM window, but browser-safe SDK bridges
|
|
4155
|
+
// address the host through `window.craft`. Keep native screens
|
|
4156
|
+
// compatible without pretending a document exists.
|
|
4157
|
+
if (typeof globalThis.window === "undefined") { globalThis.window = globalThis; }
|
|
4149
4158
|
globalThis.__stxNativeCallback = null;
|
|
4150
4159
|
globalThis.__stxNativeBridge = {
|
|
4151
4160
|
platform: "ios",
|
|
@@ -4274,7 +4283,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4274
4283
|
/// `craft.secureStorage.set` wrote.
|
|
4275
4284
|
/// - `craft.navigation.setOptions({ title, largeTitle, hidden, backTitle,
|
|
4276
4285
|
/// tabBarHidden, swipeBack, contentSwipeBack, tabSwipeEnabled,
|
|
4277
|
-
/// backgroundColor, navigationBarSolid, rightButtons })`;
|
|
4286
|
+
/// backgroundColor, navigationBarSolid, rightButtons, tabAccessory })`;
|
|
4278
4287
|
/// a right button's tap is the event `navButton`.
|
|
4279
4288
|
///
|
|
4280
4289
|
/// A runtime that assigns `craft.storage` (or the others) afterwards keeps
|
|
@@ -4529,6 +4538,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4529
4538
|
private var statusBarHiddenOverride: Bool?
|
|
4530
4539
|
/// The app's tab bar away while this screen shows (`tabBarHidden`).
|
|
4531
4540
|
private var tabBarHidden = false
|
|
4541
|
+
private var tabAccessory: [String: Any]?
|
|
4532
4542
|
/// Whether a back-swipe may leave this screen at all, the edge's
|
|
4533
4543
|
/// included (`swipeBack: false`): a screen that steps through its own
|
|
4534
4544
|
/// pages with a Back of its own (a first-run tour's questions) would
|
|
@@ -4575,6 +4585,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4575
4585
|
}
|
|
4576
4586
|
// The app script has no screen, so no bar and no tab bar to change.
|
|
4577
4587
|
guard !isAppScript else { return }
|
|
4588
|
+
if options.keys.contains("tabAccessory") {
|
|
4589
|
+
tabAccessory = options["tabAccessory"] as? [String: Any]
|
|
4590
|
+
if navigationController?.topViewController === self, view.window != nil {
|
|
4591
|
+
CraftChrome.shared.updateTabAccessory(tabAccessory)
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4578
4594
|
if options.keys.contains("title") {
|
|
4579
4595
|
navigationItem.title = options["title"] as? String
|
|
4580
4596
|
titleFromOptions = true
|
|
@@ -5719,7 +5735,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5719
5735
|
? nil : number(props["activeOpacity"]) ?? 0.5
|
|
5720
5736
|
pressable.pressBaseOpacity = number(style["opacity"]) ?? 1
|
|
5721
5737
|
pressable.pressUnderlayColor = type == "TouchableHighlight" && !disabled ? color(props["underlayColor"]) : nil
|
|
5722
|
-
pressable.pressBaseBackgroundColor = pressable.backgroundColor
|
|
5723
5738
|
case "Switch":
|
|
5724
5739
|
let toggle = result as! UISwitch
|
|
5725
5740
|
toggle.isOn = props["value"] as? Bool ?? props["checked"] as? Bool ?? false
|
|
@@ -6677,6 +6692,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6677
6692
|
updateDimension(width, constraint: &node.widthConstraint, anchor: view.widthAnchor)
|
|
6678
6693
|
updateDimension(height, constraint: &node.heightConstraint, anchor: view.heightAnchor)
|
|
6679
6694
|
view.backgroundColor = color(style["backgroundColor"]) ?? .clear
|
|
6695
|
+
// Every flow container can receive press feedback, including ordinary
|
|
6696
|
+
// Views. Capture its current authored fill before feedback cleanup or
|
|
6697
|
+
// release restores it; otherwise avatars and button wrappers go clear.
|
|
6698
|
+
(view as? CraftNativeFlowView)?.pressBaseBackgroundColor = view.backgroundColor
|
|
6680
6699
|
// A field the style dresses itself (`border-0`, a border or a
|
|
6681
6700
|
// background, as a search field inside its own pill is) draws no
|
|
6682
6701
|
// system box of its own; a bare one keeps the rounded rectangle.
|
|
@@ -6928,7 +6947,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6928
6947
|
private func textFont(_ style: [String: Any], default fallback: UIFont) -> UIFont {
|
|
6929
6948
|
let size = number(style["fontSize"]) ?? fallback.pointSize
|
|
6930
6949
|
let weight: UIFont.Weight
|
|
6931
|
-
switch style["fontWeight"]
|
|
6950
|
+
switch craftNativeFontWeight(style["fontWeight"]) {
|
|
6932
6951
|
case "100": weight = .ultraLight
|
|
6933
6952
|
case "200": weight = .thin
|
|
6934
6953
|
case "300": weight = .light
|
|
@@ -6939,11 +6958,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6939
6958
|
case "900": weight = .black
|
|
6940
6959
|
default: weight = .regular
|
|
6941
6960
|
}
|
|
6942
|
-
|
|
6943
|
-
|
|
6961
|
+
// Android's TextView uses SP, so its native text scales with the
|
|
6962
|
+
// user's content-size setting. Apply the same Dynamic Type multiplier
|
|
6963
|
+
// here before assigning the font to labels, buttons, and inputs.
|
|
6964
|
+
let scaledSize = UIFontMetrics(forTextStyle: .body).scaledValue(
|
|
6965
|
+
for: size,
|
|
6966
|
+
compatibleWith: traitCollection
|
|
6967
|
+
)
|
|
6968
|
+
var font = (style["fontFamily"] as? String).flatMap { UIFont(name: $0, size: scaledSize) }
|
|
6969
|
+
?? .systemFont(ofSize: scaledSize, weight: weight)
|
|
6944
6970
|
if style["fontStyle"] as? String == "italic",
|
|
6945
6971
|
let descriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
|
6946
|
-
font = UIFont(descriptor: descriptor, size:
|
|
6972
|
+
font = UIFont(descriptor: descriptor, size: scaledSize)
|
|
6947
6973
|
}
|
|
6948
6974
|
return font
|
|
6949
6975
|
}
|
|
@@ -7143,7 +7169,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7143
7169
|
|| controlledVideo
|
|
7144
7170
|
view.isAccessibilityElement = !container && role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "DatePicker", "Switch", "Slider"].contains(type))
|
|
7145
7171
|
view.accessibilityContainerType = container && (view.accessibilityLabel != nil || controlledVideo) ? .semanticGroup : .none
|
|
7172
|
+
// Keep the interaction trait native controls expose. Rebuilding this
|
|
7173
|
+
// set from the STX role alone otherwise turns a Slider, Picker,
|
|
7174
|
+
// DatePicker, or Switch into a static VoiceOver item whenever the
|
|
7175
|
+
// screen omits redundant accessibility metadata.
|
|
7146
7176
|
var traits: UIAccessibilityTraits = []
|
|
7177
|
+
switch type {
|
|
7178
|
+
case "Slider", "Picker", "DatePicker": traits.insert(.adjustable)
|
|
7179
|
+
case "Switch": traits.insert(.button)
|
|
7180
|
+
default: break
|
|
7181
|
+
}
|
|
7147
7182
|
switch role ?? type.lowercased() {
|
|
7148
7183
|
case "tablist": traits.insert(.tabBar)
|
|
7149
7184
|
case "button", "checkbox": traits.insert(.button)
|