craft-native 0.0.205 → 0.0.206

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.
@@ -1,6 +1,7 @@
1
1
  package {{PACKAGE_NAME}}
2
2
 
3
3
  import android.content.Context
4
+ import org.json.JSONObject
4
5
  import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
5
6
 
6
7
  /** Swipe-to-refresh host for a native FlatList. */
@@ -21,6 +22,12 @@ internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLa
21
22
  if (handler == null) isRefreshing = false
22
23
  }
23
24
 
25
+ fun setRefreshAppearance(props: JSONObject, fallback: Int) {
26
+ val tint = props.optString("tintColor").takeIf { it.isNotBlank() }
27
+ val resolved = tint?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback
28
+ setColorSchemeColors(resolved)
29
+ }
30
+
24
31
  override fun onDetachedFromWindow() {
25
32
  refreshHandler = null
26
33
  isRefreshing = false
@@ -149,8 +149,9 @@ private fun craftNativeOptionalString(value: Any?): String? = value
149
149
  ?.takeIf { it.isNotBlank() }
150
150
 
151
151
  private fun craftNativePickerValue(value: Any?): String? = when (value) {
152
+ is String -> value.takeIf { it.isNotEmpty() }
152
153
  is Number -> value.toDouble().takeIf { it.isFinite() }?.toString()
153
- else -> craftNativeOptionalString(value)
154
+ else -> null
154
155
  }
155
156
 
156
157
  private fun craftNativeEventName(events: JSONObject, vararg names: String): String =
@@ -352,6 +353,14 @@ private class CraftNativeSvgView(context: Context) : View(context) {
352
353
  val vectorEffect: String? = null,
353
354
  val dominantBaseline: String? = null,
354
355
  ) {
356
+ private fun svgTypeface(weight: String?, inherited: Typeface): Typeface = when (weight?.lowercase()) {
357
+ "500" -> Typeface.create("sans-serif-medium", Typeface.NORMAL)
358
+ "600" -> Typeface.create("sans-serif", Typeface.BOLD)
359
+ "bold", "700", "800", "900" -> Typeface.DEFAULT_BOLD
360
+ "normal", "100", "200", "300", "400" -> Typeface.DEFAULT
361
+ else -> inherited
362
+ }
363
+
355
364
  fun merge(node: JSONObject): SvgPaint {
356
365
  val props = node.optJSONObject("props") ?: JSONObject()
357
366
  val style = node.optJSONObject("style") ?: JSONObject()
@@ -387,7 +396,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
387
396
  strokeDashoffset = number("strokeDashoffset") ?: strokeDashoffset,
388
397
  strokeMiterlimit = number("strokeMiterlimit")?.takeIf { it > 0 } ?: strokeMiterlimit,
389
398
  fillRule = value("fillRule") ?: fillRule,
390
- typeface = if (weight in setOf("bold", "700")) Typeface.DEFAULT_BOLD else typeface,
399
+ typeface = svgTypeface(weight, typeface),
391
400
  strokeLinecap = value("strokeLinecap") ?: strokeLinecap,
392
401
  strokeLinejoin = value("strokeLinejoin") ?: strokeLinejoin,
393
402
  strokeDasharray = dashes(raw("strokeDasharray")) ?: strokeDasharray,
@@ -2209,13 +2218,13 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
2209
2218
  }
2210
2219
  }
2211
2220
 
2212
- fun setRefreshAppearance(props: JSONObject, density: Float) {
2221
+ fun setRefreshAppearance(props: JSONObject, density: Float, fallback: Int) {
2213
2222
  val tint = craftNativeOptionalString(props.opt("tintColor"))
2214
- if (tint != null) setColorSchemeColors(craftNativeColor(tint))
2215
- else setColorSchemeColors(Color.WHITE)
2223
+ setColorSchemeColors(tint?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback)
2216
2224
  val background = craftNativeOptionalString(props.opt("progressBackgroundColor"))
2217
- if (background != null) setProgressBackgroundColorSchemeColor(craftNativeColor(background))
2218
- else setProgressBackgroundColorSchemeColor(Color.TRANSPARENT)
2225
+ setProgressBackgroundColorSchemeColor(
2226
+ background?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: Color.TRANSPARENT,
2227
+ )
2219
2228
  val offset = props.optDouble("progressViewOffset", Double.NaN)
2220
2229
  val requested = if (offset.isFinite()) (offset * density).roundToInt() else null
2221
2230
  if (configuredProgressOffset != requested) {
@@ -2862,7 +2871,8 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
2862
2871
  if (text.isBlank() || text == JSONObject.NULL.toString()) return null
2863
2872
  val formats = listOf(
2864
2873
  "HH:mm", "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm",
2865
- "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "yyyy-MM-dd'T'HH:mm:ssX",
2874
+ "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSS",
2875
+ "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "yyyy-MM-dd'T'HH:mm:ssX",
2866
2876
  )
2867
2877
  formats.firstNotNullOfOrNull { format ->
2868
2878
  runCatching {
@@ -4485,6 +4495,13 @@ class MainActivity : AppCompatActivity() {
4485
4495
  screen.view.setBackgroundColor(configured ?: ContextCompat.getColor(this, R.color.background))
4486
4496
  }
4487
4497
 
4498
+ /** Refresh controls use a contrasting system tint when no color is authored. */
4499
+ private fun refreshTintFallback(screen: Screen): Int {
4500
+ val background = (screen.view.background as? ColorDrawable)?.color
4501
+ ?: ContextCompat.getColor(this, R.color.background)
4502
+ return if (Color.luminance(background) < 0.5f) Color.WHITE else Color.BLACK
4503
+ }
4504
+
4488
4505
  /** The bridge's capability names: Craft's core set and this host's services. */
4489
4506
  private fun capabilityNames(): JSONArray = capabilities.names().apply {
4490
4507
  services.names().forEach { name -> put(name) }
@@ -4961,7 +4978,9 @@ class MainActivity : AppCompatActivity() {
4961
4978
  val title = craftNativeOptionalString(props.opt("title")) ?: childText(children)
4962
4979
  isAllCaps = false
4963
4980
  configureText(this, title, style)
4964
- craftNativeOptionalString(props.opt("color"))?.let { setTextColor(color(it, defaultTextColor)) }
4981
+ val buttonColor = craftNativeOptionalString(props.opt("color"))
4982
+ ?: craftNativeOptionalString(style.opt("color"))
4983
+ setTextColor(buttonColor?.let { color(it, defaultTextColor) } ?: defaultTextColor)
4965
4984
  isEnabled = !props.optBoolean("disabled", false)
4966
4985
  val handler = craftNativeEventName(events, "onPress", "onClick")
4967
4986
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
@@ -4973,11 +4992,11 @@ class MainActivity : AppCompatActivity() {
4973
4992
  "Checkbox" -> (previous as? CheckBox ?: CheckBox(this)).apply {
4974
4993
  text = craftNativeOptionalString(props.opt("label")) ?: childText(children)
4975
4994
  configureText(this, text.toString(), style)
4976
- val requested = props.opt("checked") ?: props.opt("value")
4977
- val checked = when (requested) {
4978
- is Boolean -> requested
4979
- is Number -> requested.toDouble() != 0.0
4980
- is String -> requested.equals("true", ignoreCase = true) || requested == "1"
4995
+ val checkboxChecked = props.opt("checked")
4996
+ val checkboxValue = props.opt("value")
4997
+ val checked = when {
4998
+ checkboxChecked is Boolean -> checkboxChecked
4999
+ checkboxValue is Boolean -> checkboxValue
4981
5000
  else -> false
4982
5001
  }
4983
5002
  setOnCheckedChangeListener(null)
@@ -4985,11 +5004,14 @@ class MainActivity : AppCompatActivity() {
4985
5004
  // an indeterminate control must announce "mixed" and its next
4986
5005
  // user toggle should transition to checked, not unchecked.
4987
5006
  isChecked = checked
4988
- isActivated = props.optBoolean("indeterminate", false)
5007
+ val indeterminate = (props.opt("indeterminate") as? Boolean) == true
5008
+ isActivated = indeterminate
4989
5009
  isEnabled = !props.optBoolean("disabled", false)
4990
5010
  val checkboxColor = craftNativeOptionalString(props.opt("color"))
4991
5011
  ?: craftNativeOptionalString(style.opt("color"))
4992
- buttonTintList = checkboxColor?.let { ColorStateList.valueOf(color(it, defaultTextColor)) }
5012
+ buttonTintList = checkboxColor
5013
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5014
+ ?.let { ColorStateList.valueOf(it) }
4993
5015
  val handler = craftNativeEventName(events, "onValueChange", "onChange")
4994
5016
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
4995
5017
  setOnCheckedChangeListener { _, value ->
@@ -5016,11 +5038,12 @@ class MainActivity : AppCompatActivity() {
5016
5038
  hint = craftNativeOptionalString(props.opt("placeholder")).orEmpty()
5017
5039
  setHintTextColor(
5018
5040
  craftNativeOptionalString(props.opt("placeholderTextColor"))
5019
- ?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
5041
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5042
+ ?.let { ColorStateList.valueOf(it) }
5020
5043
  ?: defaultHintColors,
5021
5044
  )
5022
5045
  highlightColor = craftNativeOptionalString(props.opt("selectionColor"))
5023
- ?.let { color(it, defaultHighlightColor) }
5046
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5024
5047
  ?: defaultHighlightColor
5025
5048
  val maxLength = props.optInt("maxLength", 0)
5026
5049
  val currentLength = (filters.firstOrNull { it is InputFilter.LengthFilter } as? InputFilter.LengthFilter)?.max ?: 0
@@ -5254,24 +5277,29 @@ class MainActivity : AppCompatActivity() {
5254
5277
  val requestedValue = props.opt("value")
5255
5278
  val requestedChecked = props.opt("checked")
5256
5279
  isChecked = when {
5257
- requestedValue != null && requestedValue != JSONObject.NULL -> props.optBoolean("value")
5258
- requestedChecked == null || requestedChecked == JSONObject.NULL -> false
5280
+ requestedValue != null && requestedValue != JSONObject.NULL -> requestedValue as? Boolean ?: false
5259
5281
  requestedChecked is Boolean -> requestedChecked
5260
- requestedChecked is Number -> requestedChecked.toDouble() != 0.0
5261
- requestedChecked is String -> requestedChecked.isBlank() || requestedChecked.equals("true", ignoreCase = true)
5262
- else -> props.optBoolean("checked", false)
5282
+ else -> false
5263
5283
  }
5264
5284
  isEnabled = !props.optBoolean("disabled", false)
5265
5285
  props.optJSONObject("trackColor")?.let { track ->
5266
- val falseColor = color(track.optString("false"), Color.GRAY)
5267
- val trueColor = color(track.optString("true"), falseColor)
5268
- trackTintList = ColorStateList(
5269
- arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
5270
- intArrayOf(trueColor, falseColor),
5271
- )
5286
+ val falseRaw = craftNativeOptionalString(track.opt("false"))
5287
+ val trueRaw = craftNativeOptionalString(track.opt("true"))
5288
+ val falseColor = falseRaw?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5289
+ val trueColor = trueRaw?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5290
+ trackTintList = if ((falseRaw != null && falseColor == null) || (trueRaw != null && trueColor == null)) {
5291
+ null
5292
+ } else {
5293
+ val resolvedFalse = falseColor ?: Color.GRAY
5294
+ ColorStateList(
5295
+ arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
5296
+ intArrayOf(trueColor ?: resolvedFalse, resolvedFalse),
5297
+ )
5298
+ }
5272
5299
  } ?: run { trackTintList = null }
5273
5300
  craftNativeOptionalString(props.opt("thumbColor"))
5274
- ?.let { thumbTintList = ColorStateList.valueOf(color(it, Color.WHITE)) }
5301
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5302
+ ?.let { thumbTintList = ColorStateList.valueOf(it) }
5275
5303
  ?: run { thumbTintList = null }
5276
5304
  val handler = craftNativeEventName(events, "onValueChange", "onChange")
5277
5305
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
@@ -5308,15 +5336,15 @@ class MainActivity : AppCompatActivity() {
5308
5336
  (((value - minimum) / (maximum - minimum)) * max).roundToInt().coerceIn(0, max)
5309
5337
  }
5310
5338
  if (step != null) screen.sliderSteps[this] = step else screen.sliderSteps.remove(this)
5311
- craftNativeOptionalString(props.opt("minimumTrackTintColor"))
5312
- ?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
5313
- ?: run { progressTintList = null }
5314
- craftNativeOptionalString(props.opt("maximumTrackTintColor"))
5315
- ?.let { ColorStateList.valueOf(color(it, Color.GRAY)) }
5316
- ?: run { progressBackgroundTintList = null }
5317
- craftNativeOptionalString(props.opt("thumbTintColor"))
5318
- ?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
5319
- ?: run { thumbTintList = null }
5339
+ progressTintList = craftNativeOptionalString(props.opt("minimumTrackTintColor"))
5340
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5341
+ ?.let { ColorStateList.valueOf(it) }
5342
+ progressBackgroundTintList = craftNativeOptionalString(props.opt("maximumTrackTintColor"))
5343
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5344
+ ?.let { ColorStateList.valueOf(it) }
5345
+ thumbTintList = craftNativeOptionalString(props.opt("thumbTintColor"))
5346
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5347
+ ?.let { ColorStateList.valueOf(it) }
5320
5348
  isEnabled = !props.optBoolean("disabled", false)
5321
5349
  val handler = craftNativeEventName(events, "onValueChange", "onChange")
5322
5350
  if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
@@ -5354,9 +5382,9 @@ class MainActivity : AppCompatActivity() {
5354
5382
  val animating = props.optBoolean("animating", true)
5355
5383
  isIndeterminate = animating
5356
5384
  visibility = if (!animating && props.optBoolean("hidesWhenStopped", true)) View.GONE else View.VISIBLE
5357
- craftNativeOptionalString(props.opt("color"))
5358
- ?.let { ColorStateList.valueOf(color(it, Color.WHITE)) }
5359
- ?: run { indeterminateTintList = null }
5385
+ indeterminateTintList = craftNativeOptionalString(props.opt("color"))
5386
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5387
+ ?.let { ColorStateList.valueOf(it) }
5360
5388
  }
5361
5389
  "Icon" -> (previous as? CraftNativeIconView ?: CraftNativeIconView(this)).apply {
5362
5390
  configureIcon(this, props, style)
@@ -5386,9 +5414,9 @@ class MainActivity : AppCompatActivity() {
5386
5414
  "center" -> ImageView.ScaleType.CENTER
5387
5415
  else -> ImageView.ScaleType.CENTER_INSIDE
5388
5416
  }
5389
- craftNativeOptionalString(style.opt("tintColor"))
5390
- ?.let { image.setColorFilter(color(it, Color.TRANSPARENT)) }
5391
- ?: image.clearColorFilter()
5417
+ val tint = craftNativeOptionalString(style.opt("tintColor"))
5418
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
5419
+ if (tint == null) image.clearColorFilter() else image.setColorFilter(tint)
5392
5420
  val onLoadStart = craftNativeEventName(events, "onLoadStart")
5393
5421
  if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[image] = onLoadStart else screen.imageLoadStartHandlers.remove(image)
5394
5422
  val onLoad = craftNativeEventName(events, "onLoad")
@@ -5458,7 +5486,7 @@ class MainActivity : AppCompatActivity() {
5458
5486
  { send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
5459
5487
  }
5460
5488
  setRefreshHandler(refreshHandler, refreshProps.optBoolean("refreshing", props.optBoolean("refreshing", false)))
5461
- setRefreshAppearance(refreshProps, resources.displayMetrics.density)
5489
+ setRefreshAppearance(refreshProps, resources.displayMetrics.density, refreshTintFallback(screen))
5462
5490
  val onScroll = craftNativeEventName(events, "onScroll")
5463
5491
  if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
5464
5492
  val onBegin = craftNativeEventName(events, "onScrollBeginDrag")
@@ -5528,9 +5556,15 @@ class MainActivity : AppCompatActivity() {
5528
5556
  }
5529
5557
  "FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
5530
5558
  val nativeList = list
5531
- configureRefresh(craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }?.let { handler ->
5559
+ val propRefresh = props.optJSONObject("refreshControl") ?: JSONObject()
5560
+ val propRefreshEvents = propRefresh.optJSONObject("events") ?: JSONObject()
5561
+ val refreshHandlerName = craftNativeEventName(propRefreshEvents, "onRefresh")
5562
+ .takeIf { it.isNotBlank() }
5563
+ ?: craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }
5564
+ configureRefresh(refreshHandlerName?.let { handler ->
5532
5565
  { send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
5533
- }, props.optBoolean("refreshing", false))
5566
+ }, propRefresh.optBoolean("refreshing", props.optBoolean("refreshing", false)))
5567
+ setRefreshAppearance(propRefresh, refreshTintFallback(screen))
5534
5568
  nativeList.setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
5535
5569
  nativeList.setContentContainerStyle(props.optJSONObject("contentContainerStyle"), resources.displayMetrics.density)
5536
5570
  nativeList.setKeyboardDismissMode(props.optString("keyboardDismissMode")) {
@@ -5800,8 +5834,7 @@ class MainActivity : AppCompatActivity() {
5800
5834
  is JSONObject -> makeView(screen, child, "$path.$index", next, previousControls)
5801
5835
  is String -> TextView(this).apply {
5802
5836
  includeFontPadding = false
5803
- text = child
5804
- setTextColor(defaultTextColor)
5837
+ configureText(this, child, craftNativeTypography(style))
5805
5838
  }
5806
5839
  else -> null
5807
5840
  }
@@ -6046,10 +6079,13 @@ class MainActivity : AppCompatActivity() {
6046
6079
  }
6047
6080
  if (view is ImageView) {
6048
6081
  val tint = style.opt("tintColor") as? String
6049
- if (tint.isNullOrBlank() || tint == "null" || tint == "undefined") {
6082
+ val resolvedTint = tint
6083
+ ?.takeUnless { it.isBlank() || it == "null" || it == "undefined" }
6084
+ ?.let { runCatching { craftNativeColor(it) }.getOrNull() }
6085
+ if (resolvedTint == null) {
6050
6086
  view.clearColorFilter()
6051
6087
  } else {
6052
- view.setColorFilter(color(tint, Color.TRANSPARENT))
6088
+ view.setColorFilter(resolvedTint)
6053
6089
  }
6054
6090
  }
6055
6091
  }
@@ -6120,6 +6156,22 @@ class MainActivity : AppCompatActivity() {
6120
6156
  configureTextStyle(view, style)
6121
6157
  }
6122
6158
 
6159
+ /**
6160
+ * Raw strings inside a native container are implicit Text nodes. Keep
6161
+ * their typography, but not the parent's padding or layout, just like
6162
+ * the iOS reconciler does for the same child shape.
6163
+ */
6164
+ private fun craftNativeTypography(style: JSONObject): JSONObject = JSONObject().also { typography ->
6165
+ listOf(
6166
+ "color", "letterSpacing", "lineHeight", "fontSize", "fontFamily",
6167
+ "fontWeight", "fontStyle", "textAlign", "textTransform",
6168
+ "textDecorationLine", "textDecorationStyle", "textDecorationColor",
6169
+ "textShadowColor", "textShadowRadius", "textShadowOffset",
6170
+ ).forEach { key ->
6171
+ if (style.has(key) && !style.isNull(key)) typography.put(key, style.opt(key))
6172
+ }
6173
+ }
6174
+
6123
6175
  private fun configureIcon(view: CraftNativeIconView, props: JSONObject, style: JSONObject) {
6124
6176
  val symbol = craftNativeOptionalString(props.opt("symbol"))
6125
6177
  ?: craftNativeOptionalString(props.opt("name")).orEmpty()
@@ -6466,7 +6518,7 @@ class MainActivity : AppCompatActivity() {
6466
6518
  }
6467
6519
  ViewCompat.setStateDescription(
6468
6520
  view,
6469
- if (type == "Checkbox" && props.optBoolean("indeterminate", false)) "mixed" else null,
6521
+ if (type == "Checkbox" && (props.opt("indeterminate") as? Boolean) == true) "mixed" else null,
6470
6522
  )
6471
6523
  ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
6472
6524
  override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
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.205";
3427
+ var version = "0.0.206";
3428
3428
 
3429
3429
  // bin/cli.ts
3430
3430
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -527,6 +527,8 @@ export interface SvgPaintProps extends BaseProps {
527
527
  vectorEffect?: 'none' | 'non-scaling-stroke';
528
528
  color?: string;
529
529
  opacity?: number;
530
+ /** Font weight inherited by descendant native SVG text. */
531
+ fontWeight?: string | number;
530
532
  }
531
533
  /** Root native SVG drawing surface. */
532
534
  export interface SvgProps extends SvgPaintProps {
@@ -559,7 +561,6 @@ export interface SvgTextProps extends SvgPaintProps {
559
561
  x?: number;
560
562
  y?: number;
561
563
  fontSize?: number;
562
- fontWeight?: string | number;
563
564
  textAnchor?: 'start' | 'middle' | 'end' | string;
564
565
  dominantBaseline?: string;
565
566
  }
@@ -679,6 +680,7 @@ export interface TextInputProps extends BaseProps {
679
680
  export type KeyboardType = 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'number-pad' | 'decimal-pad' | 'url' | 'web-search' | 'visible-password';
680
681
  export type ReturnKeyType = 'done' | 'go' | 'next' | 'search' | 'send' | 'join' | 'route' | 'continue' | 'emergency-call' | 'default';
681
682
  export interface SwitchProps extends BaseProps {
683
+ /** Controlled boolean state; malformed native bridge values stay off. */
682
684
  value?: boolean;
683
685
  onValueChange?: (value: boolean) => void;
684
686
  onChange?: (value: boolean) => void;
@@ -691,6 +693,7 @@ export interface SwitchProps extends BaseProps {
691
693
  ios_backgroundColor?: string;
692
694
  }
693
695
  export interface CheckboxProps extends BaseProps {
696
+ /** Controlled boolean state; malformed native bridge values stay off. */
694
697
  value?: boolean;
695
698
  checked?: boolean;
696
699
  indeterminate?: boolean;
@@ -715,6 +718,7 @@ export interface SliderProps extends BaseProps {
715
718
  }
716
719
  export interface PickerOption {
717
720
  label: string;
721
+ /** String or finite number; other bridge values are ignored by native hosts. */
718
722
  value: string | number;
719
723
  }
720
724
  export interface PickerProps extends BaseProps {
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.205";
312
+ var version = "0.0.206";
313
313
 
314
314
  // src/sdk-version.ts
315
315
  var SDK_VERSION = version;
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.205";
60
+ var version = "0.0.206";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -102,6 +102,17 @@ function initSimulatorDriver(options) {
102
102
  if (previous !== content)
103
103
  writeFileSync(path, content);
104
104
  }
105
+ const script = join2(output, "UITests/journey.txt");
106
+ if (options.script === undefined)
107
+ rmSync(script, { force: true });
108
+ else {
109
+ let previous;
110
+ try {
111
+ previous = readFileSync(script, "utf8");
112
+ } catch {}
113
+ if (previous !== options.script)
114
+ writeFileSync(script, options.script);
115
+ }
105
116
  }
106
117
  function serializeSimulatorSteps(steps) {
107
118
  const number = (value) => {
@@ -7,6 +7,8 @@ export interface SimulatorDriverOptions {
7
7
  deploymentTarget?: string;
8
8
  controlUrl?: string;
9
9
  tabs?: readonly string[];
10
+ /** Bundle a wire-protocol journey and save native XCTest attachments. No control server is needed on physical devices. */
11
+ script?: string;
10
12
  }
11
13
  /** Generate the reusable XCUITest shell from package templates. No app-owned Swift. */
12
14
  export declare function initSimulatorDriver(options: SimulatorDriverOptions): void;
@@ -348,6 +348,10 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
348
348
  }
349
349
  }
350
350
 
351
+ func setRefreshAppearance(_ props: [String: Any]) {
352
+ nativeRefreshControl.tintColor = (props["tintColor"] as? String).flatMap { UIColor(hex: $0) }
353
+ }
354
+
351
355
  @objc private func refreshTriggered() {
352
356
  refreshHandler?()
353
357
  DispatchQueue.main.async { [weak self] in
@@ -3318,6 +3318,7 @@ private final class CraftNativeSvgView: UIView {
3318
3318
  var linejoin: String?
3319
3319
  var dashes: [CGFloat]?
3320
3320
  var vectorEffect: String?
3321
+ var fontWeight: String?
3321
3322
  var dominantBaseline: String?
3322
3323
 
3323
3324
  func merged(_ node: [String: Any]) -> Paint {
@@ -3330,6 +3331,11 @@ private final class CraftNativeSvgView: UIView {
3330
3331
  let value = CGFloat(truncating: number)
3331
3332
  return value.isFinite ? value : nil
3332
3333
  }
3334
+ func string(_ raw: Any?) -> String? {
3335
+ if let value = raw as? String { return value }
3336
+ if let number = raw as? NSNumber { return number.stringValue }
3337
+ return nil
3338
+ }
3333
3339
  if let color = style["color"] as? String { next.color = color }
3334
3340
  if let fill = style["fill"] as? String ?? props["fill"] as? String { next.fill = fill }
3335
3341
  if let stroke = style["stroke"] as? String ?? props["stroke"] as? String { next.stroke = stroke }
@@ -3346,6 +3352,7 @@ private final class CraftNativeSvgView: UIView {
3346
3352
  if let join = value("strokeLinejoin") as? String { next.linejoin = join }
3347
3353
  if let dashes = value("strokeDasharray") as? [Any] { next.dashes = dashes.compactMap(finite).filter { $0 >= 0 } }
3348
3354
  if let vectorEffect = value("vectorEffect") as? String { next.vectorEffect = vectorEffect }
3355
+ if let fontWeight = string(value("fontWeight")) { next.fontWeight = fontWeight }
3349
3356
  if let baseline = value("dominantBaseline") as? String { next.dominantBaseline = baseline }
3350
3357
  return next
3351
3358
  }
@@ -3446,7 +3453,14 @@ private final class CraftNativeSvgView: UIView {
3446
3453
  let props = node["props"] as? [String: Any] ?? [:]
3447
3454
  let style = node["style"] as? [String: Any] ?? [:]
3448
3455
  let size = number(props["fontSize"]) ?? number(style["fontSize"]) ?? 12
3449
- let weight: UIFont.Weight = (props["fontWeight"] as? String ?? style["fontWeight"] as? String).map { ["500": .medium, "600": .semibold, "700": .bold, "bold": .bold][$0] ?? .regular } ?? .regular
3456
+ let weight: UIFont.Weight = {
3457
+ switch paint.fontWeight?.lowercased() {
3458
+ case "500": return .medium
3459
+ case "600": return .semibold
3460
+ case "bold", "700", "800", "900": return .bold
3461
+ default: return .regular
3462
+ }
3463
+ }()
3450
3464
  let font = UIFont.systemFont(ofSize: size, weight: weight)
3451
3465
  let resolvedTextColor = paint.resolve(paint.fill) ?? UIColor.label
3452
3466
  let textAlpha = max(0, min(1, paint.opacity * paint.fillOpacity))
@@ -5779,6 +5793,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5779
5793
  updateImage(image, source: props["source"], defaultSource: props["defaultSource"])
5780
5794
  case "ImageBackground":
5781
5795
  let background = result as! CraftNativeImageBackgroundView
5796
+ let backgroundID = ObjectIdentifier(background)
5797
+ if let tint = color(style["tintColor"]) {
5798
+ tintedImages.insert(backgroundID)
5799
+ background.tintColor = tint
5800
+ } else {
5801
+ tintedImages.remove(backgroundID)
5802
+ background.tintColor = nil
5803
+ }
5804
+ if let current = background.image { background.image = imageForDisplay(current, view: background) }
5782
5805
  let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
5783
5806
  background.contentMode = imageContentMode(requestedResizeMode)
5784
5807
  background.clipsToBounds = background.contentMode == .scaleAspectFill
@@ -6139,6 +6162,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6139
6162
  let owner = parent.protocolId ?? path
6140
6163
  flatListOwners = flatListOwners.filter { $0.value != owner }
6141
6164
  for child in children { registerFlatListOwnership(child, owner: owner) }
6165
+ let propRefresh = props["refreshControl"] as? [String: Any] ?? [:]
6166
+ let propRefreshEvents = propRefresh["events"] as? [String: String] ?? [:]
6167
+ let refreshHandler = nonEmptyHandler(propRefreshEvents["onRefresh"])
6168
+ ?? nonEmptyHandler(events["onRefresh"])
6169
+ let refreshing = (propRefresh["refreshing"] ?? props["refreshing"]) as? Bool == true
6142
6170
  list.bounces = props["bounces"] as? Bool ?? true
6143
6171
  list.isScrollEnabled = props["scrollEnabled"] as? Bool != false
6144
6172
  list.onFittingHeightChanged = { [weak list] in
@@ -6147,9 +6175,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6147
6175
  }
6148
6176
  list.setKeyboardShouldPersistTaps(props["keyboardShouldPersistTaps"])
6149
6177
  list.setContentContainerStyle(props["contentContainerStyle"] as? [String: Any])
6150
- list.setRefreshHandler(nonEmptyHandler(events["onRefresh"]).map { [weak self] handler in
6178
+ list.setRefreshHandler(refreshHandler.map { [weak self] handler in
6151
6179
  { self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
6152
- }, refreshing: props["refreshing"] as? Bool == true)
6180
+ }, refreshing: refreshing)
6181
+ list.setRefreshAppearance(propRefresh)
6153
6182
  list.keyboardDismissMode = keyboardDismissMode(props["keyboardDismissMode"])
6154
6183
  list.onScrollEvent = nonEmptyHandler(events["onScroll"]).map { handler in
6155
6184
  { [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
@@ -42,11 +42,14 @@ import XCTest
42
42
  /// ifid <id> <command…> runs the command only when that element is there (a screen shown the first time)
43
43
  /// log <text>
44
44
  ///
45
- /// Checks log `PASS …` / `FAIL …` lines into driver.log; matrix.sh counts them.
45
+ /// With a bundled journey.txt, evidence is kept as native XCTest attachments
46
+ /// and PASS/FAIL lines appear in the test log. Control-server journeys also
47
+ /// write driver.log; matrix.sh counts those checks.
46
48
  final class DriverTests: XCTestCase {
47
49
  let control = ProcessInfo.processInfo.environment["CRAFT_TEST_CONTROL"] ?? {{CONTROL_URL}}
48
50
  let app = XCUIApplication(bundleIdentifier: {{BUNDLE_ID}})
49
51
  let tabs: [String] = {{TABS}}
52
+ private var bundledJourney = false
50
53
 
51
54
  func get(_ path: String, timeout: Double = 15) -> Data? {
52
55
  var result: Data?
@@ -57,6 +60,15 @@ final class DriverTests: XCTestCase {
57
60
  }
58
61
 
59
62
  func post(_ path: String, _ body: Data) {
63
+ if bundledJourney {
64
+ guard path.hasPrefix("/save?"),
65
+ let name = URLComponents(string: path)?.queryItems?.first(where: { $0.name == "name" })?.value else { return }
66
+ let attachment = XCTAttachment(data: body, uniformTypeIdentifier: name.hasSuffix(".png") ? "public.png" : "public.plain-text")
67
+ attachment.name = name
68
+ attachment.lifetime = .keepAlways
69
+ add(attachment)
70
+ return
71
+ }
60
72
  var request = URLRequest(url: URL(string: control + path)!)
61
73
  request.httpMethod = "POST"
62
74
  request.httpBody = body
@@ -67,7 +79,7 @@ final class DriverTests: XCTestCase {
67
79
 
68
80
  func note(_ text: String) {
69
81
  print("DRIVER \(text)")
70
- post("/note", Data(text.utf8))
82
+ if !bundledJourney { post("/note", Data(text.utf8)) }
71
83
  }
72
84
 
73
85
  func check(_ ok: Bool, _ what: String) {
@@ -156,7 +168,14 @@ final class DriverTests: XCTestCase {
156
168
 
157
169
  func testScript() throws {
158
170
  continueAfterFailure = true
159
- guard let data = get("/script"), let script = String(data: data, encoding: .utf8) else {
171
+ let data: Data?
172
+ if let resource = Bundle(for: DriverTests.self).url(forResource: "journey", withExtension: "txt") {
173
+ bundledJourney = true
174
+ data = try Data(contentsOf: resource)
175
+ } else {
176
+ data = get("/script")
177
+ }
178
+ guard let data, let script = String(data: data, encoding: .utf8) else {
160
179
  XCTFail("no script")
161
180
  return
162
181
  }
@@ -171,6 +190,10 @@ final class DriverTests: XCTestCase {
171
190
  let parts = line.split(separator: " ", maxSplits: 1).map(String.init)
172
191
  let command = parts[0]
173
192
  let arg = parts.count > 1 ? parts[1] : ""
193
+ if bundledJourney && ["net", "delay", "exec"].contains(command) {
194
+ check(false, "\(command) requires a control-server journey")
195
+ return
196
+ }
174
197
  switch command {
175
198
  case "ifid":
176
199
  let rest = arg.split(separator: " ", maxSplits: 1).map(String.init)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.205",
3
+ "version": "0.0.206",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",