craft-native 0.0.226 → 0.0.227

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.
@@ -643,6 +643,10 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
643
643
  private var accessorySpec: JSONObject? = null
644
644
  private val tabViews = mutableListOf<Pair<TextView, TextView>>()
645
645
  private val tabIconText = mutableMapOf<String, String>()
646
+ private val tabTitles = mutableMapOf<String, String>()
647
+ private val tabAccessibilityLabels = mutableMapOf<String, String>()
648
+ private val tabIconTints = mutableMapOf<String, Int>()
649
+ private val tabIconPulses = mutableSetOf<String>()
646
650
  private val tabImageURLs = mutableMapOf<String, String>()
647
651
  private val tabImages = mutableMapOf<String, android.graphics.drawable.Drawable>()
648
652
  private val navButtonSpecs = mutableMapOf<TextView, JSONObject>()
@@ -706,6 +710,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
706
710
  cell.gravity = Gravity.CENTER
707
711
  cell.background = rippleBackground()
708
712
  cell.contentDescription = tab.title
713
+ cell.tag = "craft-tab-${tab.id}"
709
714
  val icon = TextView(context)
710
715
  icon.text = CraftNativeSymbols.glyph(tab.symbol)
711
716
  icon.textSize = 22f
@@ -716,6 +721,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
716
721
  val label = TextView(context)
717
722
  label.text = tab.title
718
723
  label.textSize = 12f
724
+ label.fontFeatureSettings = "tnum"
719
725
  label.gravity = Gravity.CENTER
720
726
  label.isSingleLine = true
721
727
  cell.addView(icon, LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT))
@@ -844,12 +850,25 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
844
850
  val id = item.optString("id")
845
851
  val index = tabs.indexOfFirst { it.id == id }
846
852
  if (index < 0) return
847
- val cell = tabBar.getChildAt(index)
848
- if (item.has("accessibilityLabel")) cell.contentDescription = item.optString("accessibilityLabel").takeUnless { item.isNull("accessibilityLabel") || it.isBlank() } ?: tabs[index].title
853
+ if (item.has("accessibilityLabel")) {
854
+ tabAccessibilityLabels.remove(id)
855
+ item.optString("accessibilityLabel").takeUnless { item.isNull("accessibilityLabel") || it.isBlank() }?.let { tabAccessibilityLabels[id] = it.take(160) }
856
+ }
849
857
  if (item.has("iconText")) {
850
858
  tabIconText.remove(id)
851
859
  item.optString("iconText").takeUnless { item.isNull("iconText") || it.isBlank() }?.let { tabIconText[id] = it.take(8) }
852
860
  }
861
+ if (item.has("title")) {
862
+ tabTitles.remove(id)
863
+ item.optString("title").takeUnless { item.isNull("title") || it.isBlank() }?.let { tabTitles[id] = it.take(40) }
864
+ }
865
+ if (item.has("iconTint")) {
866
+ tabIconTints.remove(id)
867
+ if (!item.isNull("iconTint")) runCatching { Color.parseColor(item.optString("iconTint")) }.getOrNull()?.let { tabIconTints[id] = it }
868
+ }
869
+ if (item.has("iconPulse")) {
870
+ if (item.optBoolean("iconPulse", false)) tabIconPulses.add(id) else tabIconPulses.remove(id)
871
+ }
853
872
  if (item.has("image")) {
854
873
  val address = item.optString("image").takeIf { !item.isNull("image") && it.startsWith("https://") }
855
874
  if (address != tabImageURLs[id]) {
@@ -896,16 +915,41 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
896
915
  tabViews.forEachIndexed { index, (icon, label) ->
897
916
  tabBar.getChildAt(index).isSelected = index == selectedTab
898
917
  val color = if (index == selectedTab) accent else muted
899
- val number = tabIconText[tabs[index].id]
900
- icon.setTextColor(if (number != null) Color.parseColor("#FF2D55") else color)
918
+ val id = tabs[index].id
919
+ val number = tabIconText[id]
920
+ val iconColor = tabIconTints[id] ?: if (number != null) Color.parseColor("#FF2D55") else color
921
+ icon.setTextColor(iconColor)
922
+ label.text = tabTitles[id] ?: tabs[index].title
923
+ tabBar.getChildAt(index).contentDescription = tabAccessibilityLabels[id] ?: label.text
901
924
  label.setTextColor(color)
902
- val vector = tabImages[tabs[index].id] ?: if (number == null) CraftNativeIcons.drawable(context, null, tabs[index].symbol, color) else null
925
+ val vector = tabImages[id] ?: if (number == null) CraftNativeIcons.drawable(context, null, tabs[index].symbol, iconColor) else null
926
+ val pulse = tabIconPulses.contains(id) && number == null && tabImages[id] == null && hasWindowFocus() && android.animation.ValueAnimator.areAnimatorsEnabled()
927
+ if (pulse && icon.animation == null) {
928
+ icon.startAnimation(android.view.animation.AlphaAnimation(1f, 0.55f).apply {
929
+ duration = 700
930
+ repeatCount = android.view.animation.Animation.INFINITE
931
+ repeatMode = android.view.animation.Animation.REVERSE
932
+ })
933
+ } else if (!pulse) {
934
+ icon.clearAnimation()
935
+ icon.alpha = 1f
936
+ }
903
937
  icon.foreground = vector
904
938
  icon.text = if (vector != null) "" else number ?: CraftNativeSymbols.glyph(tabs[index].symbol)
905
939
  }
906
940
  navButtonSpecs.forEach { (view, spec) -> applyNavButtonTint(view, spec, dark) }
907
941
  }
908
942
 
943
+ override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
944
+ super.onWindowFocusChanged(hasWindowFocus)
945
+ if (hasWindowFocus) refreshColors() else tabViews.forEach { (icon, _) -> icon.clearAnimation() }
946
+ }
947
+
948
+ override fun onDetachedFromWindow() {
949
+ tabViews.forEach { (icon, _) -> icon.clearAnimation() }
950
+ super.onDetachedFromWindow()
951
+ }
952
+
909
953
  private fun navButton(button: JSONObject): View {
910
954
  val id = button.optString("id")
911
955
  val title = button.optString("title")
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.226";
3427
+ var version = "0.0.227";
3428
3428
 
3429
3429
  // bin/cli.ts
3430
3430
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
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.226";
312
+ var version = "0.0.227";
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.226";
60
+ var version = "0.0.227";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -616,6 +616,10 @@ final class CraftChrome: ObservableObject {
616
616
  let symbol: String
617
617
  var badge: String?
618
618
  var iconText: String? = nil
619
+ var titleOverride: String? = nil
620
+ var iconTint: String? = nil
621
+ var iconPulse = false
622
+ var displayTitle: String { titleOverride ?? title }
619
623
  var accessibilityLabel: String? = nil
620
624
  var image: String? = nil
621
625
  }
@@ -673,12 +677,21 @@ final class CraftChrome: ObservableObject {
673
677
  /// the next screen shown says again. The page's own hideTabBar is apart.
674
678
  private(set) var hiddenByScreen = false
675
679
 
676
- /// Change one tab's icon without replacing tabs, navigation stacks or selection.
680
+ /// Change one tab's caption or icon without replacing stacks or selection.
677
681
  func updateTab(_ item: [String: Any]) {
678
682
  guard let id = item["id"] as? String, let index = tabs.firstIndex(where: { $0.id == id }) else { return }
679
683
  if item.keys.contains("iconText") {
680
684
  tabs[index].iconText = (item["iconText"] as? String).flatMap { $0.isEmpty ? nil : String($0.prefix(8)) }
681
685
  }
686
+ if item.keys.contains("title") {
687
+ tabs[index].titleOverride = (item["title"] as? String).flatMap { $0.isEmpty ? nil : String($0.prefix(40)) }
688
+ }
689
+ if item.keys.contains("iconTint") {
690
+ tabs[index].iconTint = (item["iconTint"] as? String).flatMap { UIColor(hex: $0) == nil ? nil : $0 }
691
+ }
692
+ if item.keys.contains("iconPulse") {
693
+ tabs[index].iconPulse = item["iconPulse"] as? Bool ?? false
694
+ }
682
695
  if item.keys.contains("accessibilityLabel") {
683
696
  tabs[index].accessibilityLabel = (item["accessibilityLabel"] as? String).map { String($0.prefix(160)) }
684
697
  }
@@ -923,6 +936,7 @@ struct CraftTabBarView: View {
923
936
  @ObservedObject private var chrome = CraftChrome.shared
924
937
  @Namespace private var selection
925
938
  @Environment(\.accessibilityReduceMotion) private var reduceMotion
939
+ @Environment(\.scenePhase) private var scenePhase
926
940
  // Dynamic Type, relative to the styles UIKit's own bar uses. The bar is
927
941
  // capped at the largest standard size (below), as UITabBar is; past that
928
942
  // a long press shows the large content viewer instead.
@@ -1030,9 +1044,7 @@ struct CraftTabBarView: View {
1030
1044
  .minimumScaleFactor(0.7)
1031
1045
  .frame(height: iconSize + 6)
1032
1046
  } else {
1033
- Image(systemName: symbol(tab.symbol, selected: isSelected))
1034
- .font(.system(size: iconSize, weight: isSelected ? .semibold : .regular))
1035
- .frame(height: iconSize + 6)
1047
+ tabSymbol(tab, selected: isSelected)
1036
1048
  }
1037
1049
  if let badge = tab.badge {
1038
1050
  Text(badge)
@@ -1044,8 +1056,9 @@ struct CraftTabBarView: View {
1044
1056
  .offset(x: 10, y: -4)
1045
1057
  }
1046
1058
  }
1047
- Text(tab.title)
1059
+ Text(tab.displayTitle)
1048
1060
  .font(.system(size: labelSize, weight: isSelected ? .semibold : .medium))
1061
+ .monospacedDigit()
1049
1062
  .lineLimit(1)
1050
1063
  .minimumScaleFactor(0.8)
1051
1064
  }
@@ -1063,9 +1076,10 @@ struct CraftTabBarView: View {
1063
1076
  .buttonStyle(.plain)
1064
1077
  .accessibilityShowsLargeContentViewer {
1065
1078
  Image(systemName: symbol(tab.symbol, selected: isSelected))
1066
- Text(tab.title)
1079
+ Text(tab.displayTitle)
1067
1080
  }
1068
- .accessibilityLabel(tab.accessibilityLabel ?? tab.badge.map { "\(tab.title), \($0)" } ?? tab.title)
1081
+ .accessibilityIdentifier("craft-tab-\(tab.id)")
1082
+ .accessibilityLabel(tab.accessibilityLabel ?? tab.badge.map { "\(tab.displayTitle), \($0)" } ?? tab.displayTitle)
1069
1083
  .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : .isButton)
1070
1084
  }
1071
1085
  }
@@ -1075,6 +1089,20 @@ struct CraftTabBarView: View {
1075
1089
  chrome.tint.map { Color($0) } ?? Color.primary
1076
1090
  }
1077
1091
 
1092
+ /// Live styling belongs to the icon, leaving selection and the caption clear.
1093
+ @ViewBuilder private func tabSymbol(_ tab: CraftChrome.Tab, selected: Bool) -> some View {
1094
+ let color = tab.iconTint.flatMap { UIColor(hex: $0) }.map { Color($0) } ?? (selected ? selectedColor : Color.primary)
1095
+ let icon = Image(systemName: symbol(tab.symbol, selected: selected))
1096
+ .font(.system(size: iconSize, weight: selected ? .semibold : .regular))
1097
+ .foregroundStyle(color)
1098
+ .frame(height: iconSize + 6)
1099
+ if #available(iOS 17.0, *) {
1100
+ icon.symbolEffect(.pulse, options: .repeating, isActive: tab.iconPulse && !reduceMotion && scenePhase == .active)
1101
+ } else {
1102
+ icon
1103
+ }
1104
+ }
1105
+
1078
1106
  /// The filled variant for the current tab, when the symbol has one.
1079
1107
  private func symbol(_ name: String, selected: Bool) -> String {
1080
1108
  guard selected, !name.hasSuffix(".fill"), UIImage(systemName: "\(name).fill") != nil else { return name }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.226",
3
+ "version": "0.0.227",
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>",