craft-native 0.0.171 → 0.0.173

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.171";
3306
+ var version = "0.0.173";
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.171";
313
+ var version = "0.0.173";
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.171";
61
+ var version = "0.0.173";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -561,7 +561,13 @@ struct CraftApp: App {
561
561
  .ignoresSafeArea(.keyboard, edges: .bottom)
562
562
  } else if appState.config.renderer == "native" {
563
563
  // One native screen and its stack: no paths, no tabs.
564
+ // The screen keeps itself out of the keyboard's way (its
565
+ // own bottom safe area, CraftNativeScreen.swift); SwiftUI
566
+ // shrinking it as well took the keyboard's height twice,
567
+ // and a screen's scroll views and text views were squeezed
568
+ // to nothing whenever the keyboard came up.
564
569
  CraftNativeScreen(config: appState.config)
570
+ .ignoresSafeArea(.keyboard, edges: .bottom)
565
571
  } else {
566
572
  // The page's own chrome, drawn natively over it: the tab
567
573
  // bar the page asks for, and the splash until it is ready.
@@ -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
@@ -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
 
@@ -5354,6 +5398,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5354
5398
  view.accessibilityValue = [current, message].compactMap { $0 }.joined(separator: ", ")
5355
5399
  }
5356
5400
  }
5401
+ // A multiline TextInput is a UITextView, which works out its traits
5402
+ // as it goes: while it has the keyboard they say it is editing, and
5403
+ // that is what UI tests and VoiceOver read focus from. Setting them
5404
+ // froze them, so it read as static text that never had focus ("neither
5405
+ // element nor any descendant has keyboard focus"); it keeps its own.
5406
+ if view is UITextView { return }
5357
5407
  view.accessibilityTraits = traits
5358
5408
  }
5359
5409
 
@@ -0,0 +1,16 @@
1
+ export interface MacOSNativeBuildOptions {
2
+ outdir: string;
3
+ /** A compiled stx native route bundle, never HTML. */
4
+ nativeBundle: string;
5
+ zig?: string;
6
+ /** zig-js checkout and its public static archive. No system JSC fallback. */
7
+ zigJS: {
8
+ source: string;
9
+ library: string;
10
+ };
11
+ }
12
+ /** Compile Craft's AppKit host and the shared Zig layout core. No WebKit. */
13
+ export declare function buildMacOSNative(options: MacOSNativeBuildOptions): Promise<{
14
+ executable: string;
15
+ bundle: string;
16
+ }>;
@@ -0,0 +1,75 @@
1
+ // @bun
2
+ // ../macos/src/index.ts
3
+ import { cp, mkdir } from "fs/promises";
4
+ import { existsSync } from "fs";
5
+ import { join, resolve } from "path";
6
+ import process from "process";
7
+ async function buildMacOSNative(options) {
8
+ if (process.platform !== "darwin")
9
+ throw new Error("The AppKit host must be built on macOS.");
10
+ const outdir = resolve(options.outdir);
11
+ const templates = join(import.meta.dir, "../templates");
12
+ const sourceRoot = join(import.meta.dir, "../..");
13
+ const layout = existsSync(join(sourceRoot, "zig/src/native_layout.zig")) ? join(sourceRoot, "zig/src/native_layout.zig") : join(templates, "native_layout.zig");
14
+ const mutation = existsSync(join(sourceRoot, "ios/templates/CraftNativeMutation.swift")) ? join(sourceRoot, "ios/templates/CraftNativeMutation.swift") : join(templates, "CraftNativeMutation.swift");
15
+ if (!existsSync(options.nativeBundle))
16
+ throw new Error(`Native STX bundle not found: ${options.nativeBundle}`);
17
+ await mkdir(outdir, { recursive: true });
18
+ async function run(args) {
19
+ const child = Bun.spawn(args, { stdout: "inherit", stderr: "inherit" });
20
+ if (await child.exited !== 0)
21
+ throw new Error(`Native build failed: ${args[0]}`);
22
+ }
23
+ const zig = options.zig || process.env.CRAFT_ZIG || Bun.which("zig");
24
+ if (!zig)
25
+ throw new Error("Install the pinned Craft Zig toolchain or set CRAFT_ZIG.");
26
+ const layoutLibrary = join(outdir, "libcraft-layout.a");
27
+ await run([zig, "build-lib", layout, "-O", "ReleaseSafe", "-static", "-lc", `-femit-bin=${layoutLibrary}`]);
28
+ const executable = join(outdir, "craft-native-macos");
29
+ const engine = options.zigJS;
30
+ if (!existsSync(engine.library))
31
+ throw new Error(`Build zig-js first: ${engine.library}`);
32
+ if (!existsSync(join(engine.source, "include/module.modulemap")))
33
+ throw new Error("zig-js must expose its owned ZigJS Swift module.");
34
+ const bridge = join(outdir, "zig-js-objc.o");
35
+ const dispatch = join(outdir, "zig-js-dispatch.o");
36
+ const assembly = join(engine.source, `src/objc_abi_${process.arch === "arm64" ? "aarch64" : "x86_64"}.S`);
37
+ await run(["xcrun", "clang", "-fobjc-arc", "-fblocks", "-Wno-incomplete-implementation", "-I", join(engine.source, "include"), "-include", join(engine.source, "include/ZigJS.h"), "-c", join(engine.source, "src/objc_bridge.m"), "-o", bridge]);
38
+ await run([zig, "build-obj", join(engine.source, "src/objc_abi_dispatch.zig"), assembly, "-O", "ReleaseFast", `-femit-bin=${dispatch}`]);
39
+ await run([
40
+ "swiftc",
41
+ "-O",
42
+ "-swift-version",
43
+ "5",
44
+ "-I",
45
+ join(engine.source, "include"),
46
+ "-framework",
47
+ "Cocoa",
48
+ "-framework",
49
+ "AVKit",
50
+ "-framework",
51
+ "UserNotifications",
52
+ join(templates, "CraftMacRuntime.swift"),
53
+ join(templates, "CraftMacRenderer.swift"),
54
+ join(templates, "CraftMacApplication.swift"),
55
+ mutation,
56
+ bridge,
57
+ dispatch,
58
+ engine.library,
59
+ "-Xlinker",
60
+ "-force_load",
61
+ "-Xlinker",
62
+ layoutLibrary,
63
+ "-Xlinker",
64
+ "-export_dynamic",
65
+ "-o",
66
+ executable
67
+ ]);
68
+ const bundle = join(outdir, "native-screen.js");
69
+ if (resolve(options.nativeBundle) !== bundle)
70
+ await cp(options.nativeBundle, bundle);
71
+ return { executable, bundle };
72
+ }
73
+ export {
74
+ buildMacOSNative
75
+ };