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 +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +6 -0
- package/dist/ios/templates/CraftHybrid.swift +33 -18
- package/dist/ios/templates/CraftNativeScreen.swift +56 -6
- package/dist/macos/src/index.d.ts +16 -0
- package/dist/macos/src/index.js +75 -0
- package/dist/macos/templates/CraftMacApplication.swift +342 -0
- package/dist/macos/templates/CraftMacRenderer.swift +481 -0
- package/dist/macos/templates/CraftMacRuntime.swift +266 -0
- package/dist/macos/templates/CraftNativeMutation.swift +390 -0
- package/dist/macos/templates/native_layout.zig +1651 -0
- package/package.json +9 -1
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.
|
|
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
package/dist/index.js
CHANGED
|
@@ -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
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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
|
-
|
|
1630
|
+
candidate = current.superview
|
|
1626
1631
|
}
|
|
1627
|
-
return
|
|
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 =
|
|
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
|
-
|
|
1656
|
-
|
|
1657
|
-
return false
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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
|
-
//
|
|
3269
|
-
//
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
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
|
+
};
|