craft-native 0.0.170 → 0.0.172

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/cli.js CHANGED
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3303
3303
  `);
3304
3304
  }
3305
3305
  // package.json
3306
- var version = "0.0.170";
3306
+ var version = "0.0.172";
3307
3307
 
3308
3308
  // bin/cli.ts
3309
3309
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -310,7 +310,7 @@ function craftBinaryNotFoundMessage(triedPath) {
310
310
  `);
311
311
  }
312
312
  // package.json
313
- var version = "0.0.170";
313
+ var version = "0.0.172";
314
314
 
315
315
  // src/sdk-version.ts
316
316
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ function craftBinaryNotFoundMessage(triedPath) {
58
58
  `);
59
59
  }
60
60
  // package.json
61
- var version = "0.0.170";
61
+ var version = "0.0.172";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -1614,17 +1614,22 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1614
1614
  return popRecognizers(of: nav).contains { $0 === recognizer }
1615
1615
  }
1616
1616
 
1617
- private func nativeScreenContainsHorizontalScroll(_ screen: CraftNativeScreenController) -> Bool {
1618
- guard let root = screen.viewIfLoaded else { return false }
1619
- func visit(_ view: UIView) -> Bool {
1620
- if let scroll = view as? UIScrollView,
1621
- scroll.isScrollEnabled,
1622
- scroll.alwaysBounceHorizontal || scroll.contentSize.width > scroll.bounds.width + 1 {
1617
+ /// Whether the touch at `point` (in this controller's view) is on a
1618
+ /// horizontal scroll view of `root` that is past its leading edge, so a
1619
+ /// swipe to the right scrolls it back rather than leaving the screen.
1620
+ private func horizontalScrollCanScrollBack(at point: CGPoint, in root: UIView) -> Bool {
1621
+ let local = root.convert(point, from: view)
1622
+ guard root.bounds.contains(local), let hit = root.hitTest(local, with: nil) else { return false }
1623
+ var candidate: UIView? = hit
1624
+ while let current = candidate, current !== root.superview {
1625
+ if let scroll = current as? UIScrollView, scroll.isScrollEnabled,
1626
+ scroll.contentSize.width + scroll.adjustedContentInset.left + scroll.adjustedContentInset.right > scroll.bounds.width + 1,
1627
+ scroll.contentOffset.x > -scroll.adjustedContentInset.left + 1 {
1623
1628
  return true
1624
1629
  }
1625
- return view.subviews.contains(where: visit)
1630
+ candidate = current.superview
1626
1631
  }
1627
- return visit(root)
1632
+ return false
1628
1633
  }
1629
1634
 
1630
1635
  /// The edge swipe goes ahead of the web view's own gestures, which
@@ -1639,8 +1644,14 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1639
1644
 
1640
1645
  func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
1641
1646
  if tabSwipeRecognizers.contains(where: { $0 === gestureRecognizer }) {
1642
- guard hybrid.tabIds.count > 1, let top = visibleStack?.topViewController else { return false }
1647
+ guard hybrid.tabIds.count > 1, let nav = visibleStack, let top = nav.topViewController else { return false }
1643
1648
  guard CraftChrome.shared.tabSwipeEnabled else { return false }
1649
+ // Only between the tabs' own first screens, as the bar shows them:
1650
+ // deeper in a tab a swipe to the right is Back, and a screen that
1651
+ // put the bar away (a workout under way, a tour) has the whole
1652
+ // screen to itself.
1653
+ guard nav.viewControllers.count == 1, nav.transitionCoordinator == nil,
1654
+ !CraftChrome.shared.hiddenByScreen else { return false }
1644
1655
  if let pan = gestureRecognizer as? UIPanGestureRecognizer {
1645
1656
  let velocity = pan.velocity(in: view)
1646
1657
  guard abs(velocity.x) > abs(velocity.y) else { return false }
@@ -1651,15 +1662,19 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
1651
1662
  }
1652
1663
  guard let nav = visibleStack, isPopRecognizer(gestureRecognizer),
1653
1664
  nav.viewControllers.count > 1, nav.transitionCoordinator == nil else { return false }
1654
- if let native = nav.topViewController as? CraftNativeScreenController,
1655
- gestureRecognizer !== nav.interactivePopGestureRecognizer,
1656
- !native.contentSwipeBack {
1657
- return false
1658
- }
1659
- if let native = nav.topViewController as? CraftNativeScreenController,
1660
- nativeScreenContainsHorizontalScroll(native) {
1661
- let moved = (gestureRecognizer as? UIPanGestureRecognizer)?.translation(in: nav.view).x ?? 0
1662
- if gestureRecognizer.location(in: nav.view).x - moved > 32 { return false }
1665
+ if let native = nav.topViewController as? CraftNativeScreenController {
1666
+ // `swipeBack: false`: the screen's own Back is the only way back.
1667
+ if !native.swipeBack { return false }
1668
+ if gestureRecognizer !== nav.interactivePopGestureRecognizer, !native.contentSwipeBack { return false }
1669
+ // A back-swipe that starts on a pager, a carousel or a chart that
1670
+ // can still scroll back is that view's: the pager goes back a
1671
+ // page. At its first page there is nothing to scroll back to, and
1672
+ // the swipe is the screen's Back again, from the edge or (iOS 26)
1673
+ // from anywhere.
1674
+ let moved = (gestureRecognizer as? UIPanGestureRecognizer)?.translation(in: nav.view) ?? .zero
1675
+ let location = gestureRecognizer.location(in: nav.view)
1676
+ let start = CGPoint(x: location.x - moved.x, y: location.y - moved.y)
1677
+ if horizontalScrollCanScrollBack(at: nav.view.convert(start, to: view), in: native.view) { return false }
1663
1678
  }
1664
1679
  guard let slot = nav.topViewController as? CraftHybridWebSlot else { return true }
1665
1680
  // Over the page, only from its left edge: a swipe across the page is
@@ -150,6 +150,11 @@ final class CraftNativeMutationDocument {
150
150
  materialize(id, nodes: nodes)
151
151
  }
152
152
 
153
+ /// The committed parent of a node: nil for the root or a node it does not hold.
154
+ func parentId(of id: String) -> String? {
155
+ nodes[id]?.parent
156
+ }
157
+
153
158
  private func createNode(
154
159
  _ operation: [String: Any],
155
160
  nodes: inout [String: Node],
@@ -291,8 +296,21 @@ final class CraftNativeMutationDocument {
291
296
  return values.compactMap { $0 as? String }
292
297
  }
293
298
 
299
+ /// Every type the renderer draws children of (CraftNativeScreen's
300
+ /// reconcile), by the names it accepts (`canonicalType`, so `svg` is
301
+ /// `Svg`). Only View, SafeAreaView and ScrollView used to be here, so a
302
+ /// drawing revealed after the first paint (an `:if` around an `<svg>`)
303
+ /// had its shapes refused as INVALID_PARENT, and the whole screen was
304
+ /// drawn again; Android's host already took the same list.
305
+ static let containerTypes: Set<String> = [
306
+ "View", "SafeAreaView", "ScrollView", "KeyboardAvoidingView", "Modal", "Pressable",
307
+ "TouchableOpacity", "TouchableHighlight", "ImageBackground", "FlatList", "SectionList",
308
+ "Fragment", "Svg", "G",
309
+ ]
310
+ private static let containerTypeKeys = Set(containerTypes.map { $0.lowercased() })
311
+
294
312
  private func acceptsChildren(_ type: String) -> Bool {
295
- type == "View" || type == "SafeAreaView" || type == "ScrollView"
313
+ Self.containerTypeKeys.contains(type.lowercased())
296
314
  }
297
315
 
298
316
  private func descendants(of id: String, nodes: [String: Node]) -> Set<String> {
@@ -2895,7 +2895,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
2895
2895
  /// - `craft.secureStorage.getSync(key)`: the Keychain item the web page's
2896
2896
  /// `craft.secureStorage.set` wrote.
2897
2897
  /// - `craft.navigation.setOptions({ title, largeTitle, hidden, backTitle,
2898
- /// tabBarHidden, contentSwipeBack, tabSwipeEnabled, backgroundColor, rightButtons })`;
2898
+ /// tabBarHidden, swipeBack, contentSwipeBack, tabSwipeEnabled, backgroundColor, rightButtons })`;
2899
2899
  /// a right button's tap is the event `navButton`.
2900
2900
  ///
2901
2901
  /// A runtime that assigns `craft.storage` (or the others) afterwards keeps
@@ -3140,6 +3140,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3140
3140
  private var navigationBarHidden: Bool?
3141
3141
  /// The app's tab bar away while this screen shows (`tabBarHidden`).
3142
3142
  private var tabBarHidden = false
3143
+ /// Whether a back-swipe may leave this screen at all, the edge's
3144
+ /// included (`swipeBack: false`): a screen that steps through its own
3145
+ /// pages with a Back of its own (a first-run tour's questions) would
3146
+ /// otherwise lose its place to a stray swipe.
3147
+ private(set) var swipeBack = true
3143
3148
  /// Whether iOS 26's content-wide back-swipe may begin on this screen.
3144
3149
  /// Setting `contentSwipeBack: false` leaves the 32-point edge gesture.
3145
3150
  private(set) var contentSwipeBack = true
@@ -3186,6 +3191,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3186
3191
  CraftChrome.shared.setHiddenByScreen(hidden)
3187
3192
  }
3188
3193
  }
3194
+ if let swipeBack = options["swipeBack"] as? Bool {
3195
+ self.swipeBack = swipeBack
3196
+ }
3189
3197
  if let contentSwipeBack = options["contentSwipeBack"] as? Bool {
3190
3198
  self.contentSwipeBack = contentSwipeBack
3191
3199
  }
@@ -3265,11 +3273,21 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3265
3273
  button.addAction(action, for: .touchUpInside)
3266
3274
  button.translatesAutoresizingMaskIntoConstraints = false
3267
3275
  NSLayoutConstraint.activate([button.widthAnchor.constraint(equalToConstant: 44), button.heightAnchor.constraint(equalToConstant: 44)])
3268
- // The system's own fills and blue, as Apple's apps draw a bar's round
3269
- // buttons: they follow light, dark and Increase Contrast by themselves.
3270
- let accent = spec["variant"] as? String == "accent"
3271
- button.backgroundColor = accent ? UIColor.systemBlue.withAlphaComponent(0.15) : .tertiarySystemFill
3272
- button.layer.cornerRadius = 22
3276
+ // Floating, as the tab bar floats: a round piece of glass on iOS 26
3277
+ // (the bar's own material), the thin material with the tab bar's
3278
+ // hairline and shadow before it. The content scrolls on under it, and
3279
+ // it follows light, dark and Increase Contrast by itself.
3280
+ let surface = Self.floatingSurface(diameter: 44)
3281
+ button.addSubview(surface)
3282
+ // Glass lifts itself off the content; the material needs the tab
3283
+ // bar's shadow to.
3284
+ if surface.effect is UIBlurEffect {
3285
+ button.layer.shadowColor = UIColor.black.cgColor
3286
+ button.layer.shadowOpacity = 0.12
3287
+ button.layer.shadowRadius = 8
3288
+ button.layer.shadowOffset = CGSize(width: 0, height: 3)
3289
+ button.layer.shadowPath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 44, height: 44)).cgPath
3290
+ }
3273
3291
  button.tintColor = .systemBlue
3274
3292
  button.setTitleColor(button.tintColor, for: .normal)
3275
3293
  button.titleLabel?.font = .systemFont(ofSize: 13, weight: .semibold)
@@ -3314,8 +3332,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3314
3332
  }
3315
3333
  button.setTitle(nil, for: .normal)
3316
3334
  button.setImage(round.withRenderingMode(.alwaysOriginal), for: .normal)
3335
+ if let surface = button.subviews.first(where: { $0 is UIVisualEffectView }) { button.sendSubviewToBack(surface) }
3317
3336
  }
3318
3337
  }
3338
+ // Under the symbol, the initials, the picture and the badge.
3339
+ if let surface = button.subviews.first(where: { $0 is UIVisualEffectView }) { button.sendSubviewToBack(surface) }
3319
3340
  let item = UIBarButtonItem(customView: button)
3320
3341
  // iOS 26 groups adjacent bar items on one glass platter and tints
3321
3342
  // their custom views with it: the two 44-point circles came out as
@@ -3328,6 +3349,29 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3328
3349
  return item
3329
3350
  }
3330
3351
 
3352
+ /// The round surface under a bar button: glass on iOS 26, the tab bar's
3353
+ /// thin material and hairline before it. It takes no touches.
3354
+ private static func floatingSurface(diameter: CGFloat) -> UIVisualEffectView {
3355
+ let effect: UIVisualEffect
3356
+ #if compiler(>=6.2)
3357
+ if #available(iOS 26.0, *) { effect = UIGlassEffect() } else { effect = UIBlurEffect(style: .systemUltraThinMaterial) }
3358
+ #else
3359
+ effect = UIBlurEffect(style: .systemUltraThinMaterial)
3360
+ #endif
3361
+ let surface = UIVisualEffectView(effect: effect)
3362
+ surface.frame = CGRect(x: 0, y: 0, width: diameter, height: diameter)
3363
+ surface.autoresizingMask = [.flexibleWidth, .flexibleHeight]
3364
+ surface.isUserInteractionEnabled = false
3365
+ surface.layer.cornerRadius = diameter / 2
3366
+ surface.layer.cornerCurve = .circular
3367
+ surface.clipsToBounds = true
3368
+ if effect is UIBlurEffect {
3369
+ surface.layer.borderWidth = 0.5
3370
+ surface.layer.borderColor = UIColor.white.withAlphaComponent(0.35).cgColor
3371
+ }
3372
+ return surface
3373
+ }
3374
+
3331
3375
  /// A bar picture, kept for the app's life so each screen does not fetch it again.
3332
3376
  private static let barImages = NSCache<NSURL, UIImage>()
3333
3377
 
@@ -3816,7 +3860,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3816
3860
  let focused = firstResponder(in: rootStack) ?? lastFocusedInput
3817
3861
  var applied = Set<String>()
3818
3862
  for id in ids {
3819
- let target = flatListOwners[id] ?? svgOwners[id] ?? id
3863
+ guard let target = drawnTarget(id) else { return false }
3820
3864
  guard applied.insert(target).inserted else { continue }
3821
3865
  guard let document = mutationDocument.node(target), let previous = renderedNode(target) else { return false }
3822
3866
  let next = reconcile(document, identity: previous.identity, path: target, previous: previous)
@@ -3828,6 +3872,21 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3828
3872
  return true
3829
3873
  }
3830
3874
 
3875
+ /// The drawn node a change to `id` redraws: the list or drawing a row or
3876
+ /// shape belongs to, else the node itself. A node this batch created is
3877
+ /// not drawn yet (a drawing an `:if` just revealed, its shapes inserted
3878
+ /// into it), so its nearest drawn ancestor draws it, rather than the whole
3879
+ /// screen being reconciled again for want of it.
3880
+ private func drawnTarget(_ id: String) -> String? {
3881
+ var current: String? = id
3882
+ while let candidate = current {
3883
+ let target = flatListOwners[candidate] ?? svgOwners[candidate] ?? candidate
3884
+ if renderedNode(target) != nil { return target }
3885
+ current = mutationDocument.parentId(of: candidate)
3886
+ }
3887
+ return nil
3888
+ }
3889
+
3831
3890
  private func renderedNode(_ id: String, below node: RenderedNode? = nil) -> RenderedNode? {
3832
3891
  guard let node = node ?? renderedRoot else { return nil }
3833
3892
  if node.protocolId == id { return node }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.170",
3
+ "version": "0.0.172",
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>",