craft-native 0.0.248 → 0.0.249

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.
@@ -766,6 +766,8 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
766
766
  private val tabIconTints = mutableMapOf<String, Int>()
767
767
  private val tabIconPulses = mutableSetOf<String>()
768
768
  private val selectedSymbols = mutableMapOf<String, String>()
769
+ /** The page's own symbol for a tab while it says so, in place of the configured one. */
770
+ private val tabSymbols = mutableMapOf<String, String>()
769
771
  private val selectedIconTints = mutableMapOf<String, Int>()
770
772
  private val selectedIconPulses = mutableMapOf<String, Boolean>()
771
773
  private val tabImageURLs = mutableMapOf<String, String>()
@@ -1000,6 +1002,10 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
1000
1002
  if (item.has("iconPulse")) {
1001
1003
  if (item.optBoolean("iconPulse", false)) tabIconPulses.add(id) else tabIconPulses.remove(id)
1002
1004
  }
1005
+ if (item.has("symbol")) {
1006
+ tabSymbols.remove(id)
1007
+ item.optString("symbol").trim().takeUnless { item.isNull("symbol") || it.isBlank() }?.let { tabSymbols[id] = it }
1008
+ }
1003
1009
  if (item.has("selectedSymbol")) {
1004
1010
  selectedSymbols.remove(id)
1005
1011
  item.optString("selectedSymbol").trim().takeUnless { item.isNull("selectedSymbol") || it.isBlank() }?.let { selectedSymbols[id] = it }
@@ -1042,7 +1048,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
1042
1048
  val tab = tabs[index]
1043
1049
  val selected = index == selectedTab
1044
1050
  return CraftTabIconAppearance(
1045
- if (selected) selectedSymbols[tab.id] ?: tab.symbol else tab.symbol,
1051
+ if (selected) selectedSymbols[tab.id] ?: tabSymbols[tab.id] ?: tab.symbol else tabSymbols[tab.id] ?: tab.symbol,
1046
1052
  if (selected) selectedIconTints[tab.id] ?: tabIconTints[tab.id] else tabIconTints[tab.id],
1047
1053
  (if (selected) selectedIconPulses[tab.id] ?: tabIconPulses.contains(tab.id) else tabIconPulses.contains(tab.id)) && motionAllowed,
1048
1054
  )
package/dist/cli.js CHANGED
@@ -2842,7 +2842,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
2842
2842
  `);
2843
2843
  }
2844
2844
  // package.json
2845
- var version = "0.0.248";
2845
+ var version = "0.0.249";
2846
2846
 
2847
2847
  // bin/cli.ts
2848
2848
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -373,7 +373,7 @@ function craftBinaryNotFoundMessage(triedPath) {
373
373
  `);
374
374
  }
375
375
  // package.json
376
- var version = "0.0.248";
376
+ var version = "0.0.249";
377
377
 
378
378
  // src/sdk-version.ts
379
379
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
117
117
  `);
118
118
  }
119
119
  // package.json
120
- var version = "0.0.248";
120
+ var version = "0.0.249";
121
121
 
122
122
  // src/sdk-version.ts
123
123
  var SDK_VERSION = version;
@@ -648,6 +648,9 @@ final class CraftChrome: ObservableObject {
648
648
  var titleOverride: String? = nil
649
649
  var iconTint: String? = nil
650
650
  var iconPulse = false
651
+ /// The page's own symbol for the tab while it says so (a live
652
+ /// reading's filled heart), in place of the configured one.
653
+ var iconSymbol: String? = nil
651
654
  var selectedSymbol: String? = nil
652
655
  var selectedIconTint: String? = nil
653
656
  var selectedIconPulse: Bool? = nil
@@ -658,7 +661,7 @@ final class CraftChrome: ObservableObject {
658
661
  }
659
662
 
660
663
  func iconAppearance(selected: Bool, motionAllowed: Bool) -> (symbol: String, tint: String?, pulse: Bool) {
661
- let name = selected ? selectedSymbol ?? symbol : symbol
664
+ let name = selected ? selectedSymbol ?? iconSymbol ?? symbol : iconSymbol ?? symbol
662
665
  let tint = selected ? selectedIconTint ?? iconTint : iconTint
663
666
  let pulse = selected ? selectedIconPulse ?? iconPulse : iconPulse
664
667
  return (name, tint, pulse && motionAllowed)
@@ -750,6 +753,9 @@ final class CraftChrome: ObservableObject {
750
753
  if item.keys.contains("iconPulse") {
751
754
  next.iconPulse = item["iconPulse"] as? Bool ?? false
752
755
  }
756
+ if item.keys.contains("symbol") {
757
+ next.iconSymbol = Tab.normalizedSelectedSymbol(item["symbol"] as? String)
758
+ }
753
759
  if item.keys.contains("selectedSymbol") {
754
760
  next.selectedSymbol = Tab.normalizedSelectedSymbol(item["selectedSymbol"] as? String)
755
761
  }
@@ -1249,8 +1249,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1249
1249
  // create a second web root here: the shared WebView cannot draw
1250
1250
  // both pages during the same gesture.
1251
1251
  guard hybrid.routes.match(id) != nil else { return false }
1252
- target = CraftFramePacing.shared.measure("tab swipe mount \(id)") { makeStack(tab: id, rootPath: id) }
1253
- (target.topViewController as? CraftNativeScreenController)?.screenVisibilityChanged(false)
1252
+ target = CraftFramePacing.shared.measure("tab swipe mount \(id)") { makeStack(tab: id, rootPath: id, hidden: true) }
1254
1253
  }
1255
1254
  guard current.view !== target.view,
1256
1255
  !(current.topViewController is CraftHybridWebSlot && target.topViewController is CraftHybridWebSlot),
@@ -1546,6 +1545,12 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1546
1545
 
1547
1546
  private var visibleStack: UINavigationController? { stacks[currentTab] }
1548
1547
 
1548
+ /// A swipe preview can be visible without owning navigation or focus.
1549
+ /// UIKit appearance callbacks must follow the committed native stack.
1550
+ func isFocusedScreen(_ screen: CraftNativeScreenController) -> Bool {
1551
+ visibleStack?.topViewController === screen
1552
+ }
1553
+
1549
1554
  var webIsVisible: Bool {
1550
1555
  guard let webHost, let slot = visibleStack?.topViewController as? CraftHybridWebSlot else { return false }
1551
1556
  return webHost.view.superview === slot.view
@@ -1557,7 +1562,11 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1557
1562
  /// the view hierarchy, so its screen never takes the shell's chrome (the
1558
1563
  /// bar, the tab accessory) from the one on screen.
1559
1564
  private func makeStack(tab: String, rootPath: String, hidden: Bool = false) -> UINavigationController {
1560
- let nav = UINavigationController(rootViewController: entry(for: rootPath, isTabRoot: true, depth: nil))
1565
+ let root = entry(for: rootPath, isTabRoot: true, depth: nil)
1566
+ // Constructing the navigation controller can load the root's view and
1567
+ // mount its script immediately. Set its initial focus before that.
1568
+ if hidden { (root as? CraftNativeScreenController)?.screenVisibilityChanged(false) }
1569
+ let nav = UINavigationController(rootViewController: root)
1561
1570
  nav.delegate = self
1562
1571
  nav.view.backgroundColor = config.resolvedBackgroundColor
1563
1572
  nav.additionalSafeAreaInsets.bottom = safeAreaForTabBar()
@@ -3993,6 +3993,9 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
3993
3993
  required init?(coder: NSCoder) { nil }
3994
3994
 
3995
3995
  override func touchesShouldCancel(in view: UIView) -> Bool {
3996
+ // A drag beginning on a button scrolls; lifting without dragging
3997
+ // remains a tap. Sliders, switches and text fields keep tracking.
3998
+ if view is UIButton { return true }
3996
3999
  if view is UIControl { return false }
3997
4000
  return super.touchesShouldCancel(in: view)
3998
4001
  }
@@ -5143,6 +5146,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5143
5146
  /// sees, not a tab prepared behind it.
5144
5147
  private var isShownOnScreen: Bool {
5145
5148
  guard let window = view.window else { return false }
5149
+ if let shell = navigationController?.parent as? CraftHybridController,
5150
+ !shell.isFocusedScreen(self) { return false }
5146
5151
  var candidate: UIView? = view
5147
5152
  while let current = candidate, current !== window {
5148
5153
  if current.isHidden { return false }
@@ -5326,13 +5331,14 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5326
5331
  }
5327
5332
 
5328
5333
  /// What a screen's script starts with that only the main thread may read:
5329
- /// the app's state, the appearance, what the device can do.
5334
+ /// the app's state, the appearance, initial visibility, what the device can do.
5330
5335
  private struct ScriptInputs {
5331
5336
  let capabilityJSON: String
5332
5337
  let appState: String
5333
5338
  let colorScheme: String
5334
5339
  let appearanceJSON: String
5335
5340
  let sessionTokenKey: String
5341
+ let focused: Bool?
5336
5342
  }
5337
5343
 
5338
5344
  private func scriptInputs() -> ScriptInputs {
@@ -5343,7 +5349,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5343
5349
  appState: CraftNativeActions.currentAppState(),
5344
5350
  colorScheme: colorScheme,
5345
5351
  appearanceJSON: appearanceJSON(),
5346
- sessionTokenKey: Self.jsonString(CraftSession.tokenKey(config))
5352
+ sessionTokenKey: Self.jsonString(CraftSession.tokenKey(config)),
5353
+ focused: reportedVisible
5347
5354
  )
5348
5355
  }
5349
5356
 
@@ -5355,7 +5362,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5355
5362
  jsContext.setObject(routeProps as NSDictionary, forKeyedSubscript: "__stxNativeProps" as NSString)
5356
5363
  // Hidden before its script ran: the SCREEN_FOCUS saying so reached no
5357
5364
  // callback, so the script reads it as it starts instead.
5358
- if reportedVisible == false {
5365
+ if inputs.focused == false {
5359
5366
  jsContext.setObject(false, forKeyedSubscript: "__stxNativeFocused" as NSString)
5360
5367
  }
5361
5368
  jsContext.exceptionHandler = { [weak self] context, exception in
@@ -9097,7 +9104,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
9097
9104
  // A video with its system controls is a group too: VoiceOver reaches
9098
9105
  // play, the scrubber, skipping and full screen inside it.
9099
9106
  let controlledVideo = type == "Video" && (props["controls"] as? Bool) == true
9100
- let container = ["ScrollView", "FlatList", "SectionList"].contains(type)
9107
+ // UIPickerView exposes adjustable component wheels. Making the
9108
+ // picker itself a leaf hides those wheels and their selected values.
9109
+ let container = ["ScrollView", "FlatList", "SectionList", "Picker"].contains(type)
9101
9110
  || ["tablist", "radiogroup", "list", "menu", "menubar", "toolbar"].contains(role ?? "")
9102
9111
  || controlledVideo
9103
9112
  view.isAccessibilityElement = !container && role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "DatePicker", "Switch", "Slider"].contains(type))
@@ -9108,7 +9117,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
9108
9117
  // screen omits redundant accessibility metadata.
9109
9118
  var traits: UIAccessibilityTraits = []
9110
9119
  switch type {
9111
- case "Slider", "Picker", "DatePicker": traits.insert(.adjustable)
9120
+ case "Slider", "DatePicker": traits.insert(.adjustable)
9112
9121
  default: break
9113
9122
  }
9114
9123
  switch role ?? type.lowercased() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.248",
3
+ "version": "0.0.249",
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>",