craft-native 0.0.346 → 0.0.350

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.
@@ -29,7 +29,7 @@
29
29
  * @returns The string to pass to `spawn(...)`. Either `explicit`,
30
30
  * `process.env.CRAFT_BIN`, or the bare `'craft'` for PATH lookup.
31
31
  */
32
- export declare function resolveCraftBinary(explicit?: string): string;
32
+ export declare function resolveCraftBinary(explicit?: string, profile?: 'full' | 'webview'): string;
33
33
  /**
34
34
  * Set on the child environment whenever the CLI spawns what it believes is
35
35
  * the native binary.
package/dist/cli.js CHANGED
@@ -242,7 +242,9 @@ async function packageApp(config) {
242
242
  return results;
243
243
  }
244
244
  function macOSAppStoreRuntimePath(providerPath) {
245
- const runtime = basename(providerPath) === "craft-app-store" ? providerPath : join2(dirname(providerPath), "craft-app-store");
245
+ const providerName = basename(providerPath);
246
+ const storeName = providerName.startsWith("craft-webview") ? "craft-webview-app-store" : "craft-app-store";
247
+ const runtime = providerName === storeName ? providerPath : join2(dirname(providerPath), storeName);
246
248
  if (!existsSync4(runtime))
247
249
  throw new Error("App Store packaging requires Pantry\u2019s craft-app-store runtime. Upgrade Craft with Pantry before building.");
248
250
  const bytes = readFileSync2(runtime);
@@ -299,6 +301,17 @@ async function packageMacOS(config, outDir) {
299
301
  }
300
302
  try {
301
303
  const contents = join2(appBundlePath, "Contents");
304
+ if (opts.appStore && opts.appBundlePath && existsSync4(join2(contents, "Resources", "craft.json"))) {
305
+ if (!nativeRuntimePath)
306
+ throw new Error("App Store packaging of a prebuilt Craft bundle requires macos.nativeRuntimePath from Pantry.");
307
+ const plist = join2(contents, "Info.plist");
308
+ const executable = spawnSync("plutil", ["-extract", "CFBundleExecutable", "raw", "-o", "-", plist], { encoding: "utf8" });
309
+ const name = executable.stdout.trim();
310
+ if (executable.status !== 0 || !name || basename(name) !== name || name === "." || name === "..")
311
+ throw new Error("Prebuilt Craft bundle has an invalid CFBundleExecutable");
312
+ copyFileSync(nativeRuntimePath, join2(contents, "MacOS", name));
313
+ chmodSync(join2(contents, "MacOS", name), 493);
314
+ }
302
315
  if (opts.provisioningProfile) {
303
316
  const profile = join2(contents, "embedded.provisionprofile");
304
317
  copyFileSync(opts.provisioningProfile, profile);
@@ -2947,7 +2960,7 @@ import process6 from "process";
2947
2960
 
2948
2961
  // src/binary-resolver.ts
2949
2962
  import { existsSync as existsSync2 } from "fs";
2950
- function resolveCraftBinary(explicit) {
2963
+ function resolveCraftBinary(explicit, profile = "full") {
2951
2964
  if (explicit) {
2952
2965
  if (!existsSync2(explicit)) {
2953
2966
  throw new Error(`Custom Craft binary path not found: ${explicit}`);
@@ -2961,7 +2974,7 @@ function resolveCraftBinary(explicit) {
2961
2974
  }
2962
2975
  return fromEnv;
2963
2976
  }
2964
- return "craft";
2977
+ return profile === "webview" ? "craft-webview" : "craft";
2965
2978
  }
2966
2979
  var CRAFT_CLI_SPAWN_MARKER = "CRAFT_CLI_SPAWNED_FROM";
2967
2980
  function craftBinaryIsCliShimMessage(spawnedFrom) {
@@ -3022,7 +3035,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3022
3035
  `);
3023
3036
  }
3024
3037
  // package.json
3025
- var version = "0.0.346";
3038
+ var version = "0.0.350";
3026
3039
 
3027
3040
  // bin/cli.ts
3028
3041
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -341,7 +341,7 @@ var import_node_util = require("node:util");
341
341
 
342
342
  // src/binary-resolver.ts
343
343
  var import_node_fs = require("node:fs");
344
- function resolveCraftBinary(explicit) {
344
+ function resolveCraftBinary(explicit, profile = "full") {
345
345
  if (explicit) {
346
346
  if (!import_node_fs.existsSync(explicit)) {
347
347
  throw new Error(`Custom Craft binary path not found: ${explicit}`);
@@ -355,7 +355,7 @@ function resolveCraftBinary(explicit) {
355
355
  }
356
356
  return fromEnv;
357
357
  }
358
- return "craft";
358
+ return profile === "webview" ? "craft-webview" : "craft";
359
359
  }
360
360
  var CRAFT_CLI_SPAWN_MARKER = "CRAFT_CLI_SPAWNED_FROM";
361
361
  function craftBinaryNotFoundMessage(triedPath) {
@@ -375,7 +375,7 @@ function craftBinaryNotFoundMessage(triedPath) {
375
375
  `);
376
376
  }
377
377
  // package.json
378
- var version = "0.0.346";
378
+ var version = "0.0.350";
379
379
 
380
380
  // src/sdk-version.ts
381
381
  var SDK_VERSION = version;
@@ -608,7 +608,9 @@ async function packageApp(config) {
608
608
  return results;
609
609
  }
610
610
  function macOSAppStoreRuntimePath(providerPath) {
611
- const runtime = import_path.basename(providerPath) === "craft-app-store" ? providerPath : import_path.join(import_path.dirname(providerPath), "craft-app-store");
611
+ const providerName = import_path.basename(providerPath);
612
+ const storeName = providerName.startsWith("craft-webview") ? "craft-webview-app-store" : "craft-app-store";
613
+ const runtime = providerName === storeName ? providerPath : import_path.join(import_path.dirname(providerPath), storeName);
612
614
  if (!import_fs.existsSync(runtime))
613
615
  throw new Error("App Store packaging requires Pantry’s craft-app-store runtime. Upgrade Craft with Pantry before building.");
614
616
  const bytes = import_fs.readFileSync(runtime);
@@ -665,6 +667,17 @@ async function packageMacOS(config, outDir) {
665
667
  }
666
668
  try {
667
669
  const contents = import_path.join(appBundlePath, "Contents");
670
+ if (opts.appStore && opts.appBundlePath && import_fs.existsSync(import_path.join(contents, "Resources", "craft.json"))) {
671
+ if (!nativeRuntimePath)
672
+ throw new Error("App Store packaging of a prebuilt Craft bundle requires macos.nativeRuntimePath from Pantry.");
673
+ const plist = import_path.join(contents, "Info.plist");
674
+ const executable = import_child_process.spawnSync("plutil", ["-extract", "CFBundleExecutable", "raw", "-o", "-", plist], { encoding: "utf8" });
675
+ const name = executable.stdout.trim();
676
+ if (executable.status !== 0 || !name || import_path.basename(name) !== name || name === "." || name === "..")
677
+ throw new Error("Prebuilt Craft bundle has an invalid CFBundleExecutable");
678
+ import_fs.copyFileSync(nativeRuntimePath, import_path.join(contents, "MacOS", name));
679
+ import_fs.chmodSync(import_path.join(contents, "MacOS", name), 493);
680
+ }
668
681
  if (opts.provisioningProfile) {
669
682
  const profile = import_path.join(contents, "embedded.provisionprofile");
670
683
  import_fs.copyFileSync(opts.provisioningProfile, profile);
@@ -11785,7 +11798,7 @@ class CraftApp {
11785
11798
  return args;
11786
11799
  }
11787
11800
  async findCraftBinary() {
11788
- return resolveCraftBinary(this.config.craftPath);
11801
+ return resolveCraftBinary(this.config.craftPath, this.config.runtimeProfile);
11789
11802
  }
11790
11803
  }
11791
11804
  function createApp(config) {
package/dist/index.js CHANGED
@@ -83,7 +83,7 @@ import { promisify as promisify2 } from "util";
83
83
 
84
84
  // src/binary-resolver.ts
85
85
  import { existsSync } from "fs";
86
- function resolveCraftBinary(explicit) {
86
+ function resolveCraftBinary(explicit, profile = "full") {
87
87
  if (explicit) {
88
88
  if (!existsSync(explicit)) {
89
89
  throw new Error(`Custom Craft binary path not found: ${explicit}`);
@@ -97,7 +97,7 @@ function resolveCraftBinary(explicit) {
97
97
  }
98
98
  return fromEnv;
99
99
  }
100
- return "craft";
100
+ return profile === "webview" ? "craft-webview" : "craft";
101
101
  }
102
102
  var CRAFT_CLI_SPAWN_MARKER = "CRAFT_CLI_SPAWNED_FROM";
103
103
  function craftBinaryNotFoundMessage(triedPath) {
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
117
117
  `);
118
118
  }
119
119
  // package.json
120
- var version = "0.0.346";
120
+ var version = "0.0.350";
121
121
 
122
122
  // src/sdk-version.ts
123
123
  var SDK_VERSION = version;
@@ -364,7 +364,9 @@ async function packageApp(config) {
364
364
  return results;
365
365
  }
366
366
  function macOSAppStoreRuntimePath(providerPath) {
367
- const runtime = basename(providerPath) === "craft-app-store" ? providerPath : join2(dirname(providerPath), "craft-app-store");
367
+ const providerName = basename(providerPath);
368
+ const storeName = providerName.startsWith("craft-webview") ? "craft-webview-app-store" : "craft-app-store";
369
+ const runtime = providerName === storeName ? providerPath : join2(dirname(providerPath), storeName);
368
370
  if (!existsSync3(runtime))
369
371
  throw new Error("App Store packaging requires Pantry\u2019s craft-app-store runtime. Upgrade Craft with Pantry before building.");
370
372
  const bytes = readFileSync(runtime);
@@ -421,6 +423,17 @@ async function packageMacOS(config, outDir) {
421
423
  }
422
424
  try {
423
425
  const contents = join2(appBundlePath, "Contents");
426
+ if (opts.appStore && opts.appBundlePath && existsSync3(join2(contents, "Resources", "craft.json"))) {
427
+ if (!nativeRuntimePath)
428
+ throw new Error("App Store packaging of a prebuilt Craft bundle requires macos.nativeRuntimePath from Pantry.");
429
+ const plist = join2(contents, "Info.plist");
430
+ const executable = spawnSync("plutil", ["-extract", "CFBundleExecutable", "raw", "-o", "-", plist], { encoding: "utf8" });
431
+ const name = executable.stdout.trim();
432
+ if (executable.status !== 0 || !name || basename(name) !== name || name === "." || name === "..")
433
+ throw new Error("Prebuilt Craft bundle has an invalid CFBundleExecutable");
434
+ copyFileSync(nativeRuntimePath, join2(contents, "MacOS", name));
435
+ chmodSync(join2(contents, "MacOS", name), 493);
436
+ }
424
437
  if (opts.provisioningProfile) {
425
438
  const profile = join2(contents, "embedded.provisionprofile");
426
439
  copyFileSync(opts.provisioningProfile, profile);
@@ -11541,7 +11554,7 @@ class CraftApp {
11541
11554
  return args;
11542
11555
  }
11543
11556
  async findCraftBinary() {
11544
- return resolveCraftBinary(this.config.craftPath);
11557
+ return resolveCraftBinary(this.config.craftPath, this.config.runtimeProfile);
11545
11558
  }
11546
11559
  }
11547
11560
  function createApp(config) {
@@ -18,12 +18,16 @@ final class CraftMacScene: NSObject, NSWindowDelegate {
18
18
  super.init()
19
19
  runtime.scene = self; renderer.window = window
20
20
  window.title = options["title"] as? String ?? application.appName
21
+ if (options["titlebarStyle"] as? String ?? application.titlebarStyle) == "transparent" {
22
+ window.titlebarAppearsTransparent = true
23
+ window.titleVisibility = .hidden
24
+ }
21
25
  window.minSize = CGSize(width: number(options["minWidth"]) ?? 640, height: number(options["minHeight"]) ?? 400)
22
26
  window.isReleasedWhenClosed = false
23
27
  renderer.root.autoresizingMask = [.width, .height]
24
28
  window.contentView = renderer.root
25
29
  window.delegate = self; window.center()
26
- if name == "main" { window.setFrameAutosaveName("\(application.appName)NativeMain") }
30
+ if name == "main" && application.testDirectory == nil { window.setFrameAutosaveName("\(application.appName)NativeMain") }
27
31
  window.makeKeyAndOrderFront(nil)
28
32
  runtime.load(application.bundle)
29
33
  }
@@ -49,6 +53,8 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
49
53
  var appName = "Craft"
50
54
  var bundle = ""
51
55
  var initialRoute = "Main"
56
+ var titlebarStyle = "standard"
57
+ var mainWindowOptions: [String: Any] = [:]
52
58
  var network: CraftMacNetwork!
53
59
  var scenes: [String: CraftMacScene] = [:]
54
60
  var panels: [NSWindow] = []
@@ -73,7 +79,7 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
73
79
  guard error == nil, (response as? HTTPURLResponse)?.statusCode == 200 else {
74
80
  let alert = NSAlert(); alert.messageText = "Could not connect to the app"; alert.informativeText = error?.localizedDescription ?? "The local agent did not issue a native session."; alert.runModal(); NSApp.terminate(nil); return
75
81
  }
76
- self.open(name: "main", route: self.initialRoute, options: ["title": self.appName])
82
+ self.open(name: "main", route: self.initialRoute, options: self.mainWindowOptions.merging(["title": self.appName]) { _, new in new })
77
83
  self.startTestDriver()
78
84
  }
79
85
  }.resume()
@@ -91,6 +97,8 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
91
97
  }
92
98
  func broadcast(_ id: String) { for scene in scenes.values { scene.runtime.send("CRAFT_EVENT", ["event": "craftDesktopCommand", "detail": ["id": id]]) } }
93
99
  func appearanceChanged() {
100
+ CATransaction.begin(); CATransaction.setDisableActions(true)
101
+ defer { CATransaction.commit() }
94
102
  let scheme = CraftMacColor.isDark ? "dark" : "light"
95
103
  for scene in scenes.values {
96
104
  scene.runtime.context.evaluateScript("__stxNativeBridge.colorScheme = '\(scheme)';")
@@ -266,6 +274,11 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
266
274
  let scene = scenes[action["window"] as? String ?? "main"]
267
275
  guard let scene else { return ["ok": false, "error": "Window not found"] }
268
276
  if kind == "close" { scene.window.performClose(nil); return ["ok": true] }
277
+ if kind == "resize", let width = number(action["width"]), let height = number(action["height"]), width.isFinite, height.isFinite {
278
+ scene.window.setContentSize(CGSize(width: max(scene.window.minSize.width, width), height: max(scene.window.minSize.height, height)))
279
+ scene.renderer.root.layoutSubtreeIfNeeded()
280
+ return ["ok": true]
281
+ }
269
282
  if kind == "screenshot" {
270
283
  guard let directory = testDirectory, let view = scene.window.contentView?.superview,
271
284
  let bitmap = view.bitmapImageRepForCachingDisplay(in: view.bounds) else { return ["ok": false, "error": "Window cannot be rendered"] }
@@ -305,9 +318,15 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
305
318
  let windows: [[String: Any]] = scenes.values.map { scene in
306
319
  let nodes: [[String: Any]] = scene.renderer.boxes.values.map { box in
307
320
  var node: [String: Any] = ["id": box.props["testID"] as? String ?? box.props["testId"] as? String ?? box.id, "type": box.kind,
308
- "nativeClass": box.control.map { String(describing: type(of: $0)) } ?? "NSView", "visible": !box.isHidden && box.window?.isVisible == true && box.frame.width > 0 && box.frame.height > 0,
321
+ "nativeClass": box.control.map { String(describing: type(of: $0)) } ?? "NSView", "visible": !box.isHiddenOrHasHiddenAncestor && box.window?.isVisible == true && !box.visibleRect.isEmpty,
309
322
  "frame": [box.frame.minX, box.frame.minY, box.frame.width, box.frame.height]]
310
323
  if let field = box.control as? NSTextField { node["value"] = field.stringValue }
324
+ if let field = box.control as? NSTextField { node["fontSize"] = field.font?.pointSize; node["lines"] = field.maximumNumberOfLines }
325
+ let frame = box.convert(box.bounds, to: scene.renderer.root)
326
+ node["windowFrame"] = [frame.minX, frame.minY, frame.width, frame.height]
327
+ node["style"] = box.style
328
+ node["hiddenAncestor"] = box.isHiddenOrHasHiddenAncestor
329
+ node["visibleRect"] = [box.visibleRect.minX, box.visibleRect.minY, box.visibleRect.width, box.visibleRect.height]
311
330
  if let text = (box.control as? NSScrollView)?.documentView as? NSTextView { node["value"] = text.string; node["editorClass"] = NSStringFromClass(type(of: text)) }
312
331
  if let button = box.control as? NSButton { node["label"] = button.title; node["enabled"] = button.isEnabled }
313
332
  if let picker = box.control as? NSPopUpButton { node["value"] = picker.selectedItem?.representedObject ?? "" }
@@ -333,6 +352,10 @@ final class CraftMacApplication: NSObject, NSApplicationDelegate, UNUserNotifica
333
352
  application.bundle = try String(contentsOfFile: bundlePath, encoding: .utf8)
334
353
  application.appName = argument("--title", fallback: "Craft")
335
354
  application.initialRoute = argument("--route", fallback: "Main")
355
+ application.titlebarStyle = argument("--titlebar-style", fallback: "standard")
356
+ for (flag, key) in [("--width", "width"), ("--height", "height"), ("--min-width", "minWidth"), ("--min-height", "minHeight")] {
357
+ if let value = Double(argument(flag)), value.isFinite, value > 0 { application.mainWindowOptions[key] = value }
358
+ }
336
359
  application.network = CraftMacNetwork(base: api)
337
360
  let testDirectory = argument("--test-dir")
338
361
  if !testDirectory.isEmpty { application.testDirectory = URL(fileURLWithPath: testDirectory, isDirectory: true) }
@@ -45,12 +45,20 @@ enum CraftMacFlex {
45
45
  final class CraftMacFlow: NSView {
46
46
  var boxes: [CraftMacBox] = []
47
47
  var style: [String: Any] = [:]
48
+ var componentScope = false
48
49
  override var isFlipped: Bool { true }
49
50
  override func layout() {
50
51
  super.layout()
51
52
  layoutBoxes(in: bounds.size)
52
53
  }
53
54
  func layoutBoxes(in size: CGSize) {
55
+ // AppKit can lay out this inner flow after its owning box. Component
56
+ // scopes must preserve the root's allocation on that later pass too.
57
+ if componentScope, boxes.count == 1, boxes[0].kind != "Modal", boxes[0].superview === self {
58
+ boxes[0].frame = CGRect(origin: .zero, size: size)
59
+ boxes[0].layoutSubtreeIfNeeded()
60
+ return
61
+ }
54
62
  let horizontal = style["flexDirection"] as? String == "row"
55
63
  let padding = dimension(style["padding"], base: size.width) ?? 0
56
64
  let left = dimension(style["paddingLeft"], base: size.width) ?? dimension(style["paddingHorizontal"], base: size.width) ?? padding
@@ -58,41 +66,75 @@ final class CraftMacFlow: NSView {
58
66
  let top = dimension(style["paddingTop"], base: size.height) ?? dimension(style["paddingVertical"], base: size.height) ?? padding
59
67
  let bottom = dimension(style["paddingBottom"], base: size.height) ?? dimension(style["paddingVertical"], base: size.height) ?? padding
60
68
  let inner = CGSize(width: max(0, size.width - left - right), height: max(0, size.height - top - bottom))
61
- let visible = boxes.filter { !$0.isHidden && $0.kind != "Modal" }
69
+ let visible = boxes.filter { !$0.isHidden && $0.kind != "Modal" && $0.layoutStyle["position"] as? String != "absolute" }
62
70
  let gap = dimension(style["gap"], base: inner.width) ?? 0
63
- let sizes = visible.map { $0.measure(width: inner.width) }
71
+ let sizes = visible.map { box in
72
+ let alignment = box.layoutStyle["alignSelf"] as? String ?? style["alignItems"] as? String ?? "stretch"
73
+ return box.measure(width: inner.width, stretch: !horizontal && alignment == "stretch")
74
+ }
64
75
  let items = visible.enumerated().map { index, box -> CraftMacFlex.Item in
65
76
  let main = horizontal ? sizes[index].width : sizes[index].height
66
- let basis = dimension(box.style["flexBasis"], base: horizontal ? inner.width : inner.height) ?? main
67
- let grow = number(box.style["flexGrow"]) ?? number(box.style["flex"]) ?? 0
68
- let shrink = number(box.style["flexShrink"]) ?? (grow > 0 ? 1 : 0)
69
- let minimum = dimension(box.style[horizontal ? "minWidth" : "minHeight"], base: horizontal ? inner.width : inner.height)
70
- let maximum = dimension(box.style[horizontal ? "maxWidth" : "maxHeight"], base: horizontal ? inner.width : inner.height)
71
- return .init(basis: Float(basis), grow: Float(grow), shrink: Float(shrink), min: minimum.map(Float.init) ?? .nan, max: maximum.map(Float.init) ?? .nan, start: 0, end: 0)
77
+ let layout = box.layoutStyle
78
+ let grow = number(layout["flexGrow"]) ?? number(layout["flex"]) ?? 0
79
+ let basis = dimension(layout["flexBasis"], base: horizontal ? inner.width : inner.height) ?? (number(layout["flex"]) != nil ? 0 : main)
80
+ let shrink = number(layout["flexShrink"]) ?? (grow > 0 ? 1 : 0)
81
+ let minimum = dimension(layout[horizontal ? "minWidth" : "minHeight"], base: horizontal ? inner.width : inner.height)
82
+ let maximum = dimension(layout[horizontal ? "maxWidth" : "maxHeight"], base: horizontal ? inner.width : inner.height)
83
+ let margin = number(layout["margin"]) ?? 0
84
+ let start = number(layout[horizontal ? "marginLeft" : "marginTop"]) ?? number(layout[horizontal ? "marginHorizontal" : "marginVertical"]) ?? margin
85
+ let end = number(layout[horizontal ? "marginRight" : "marginBottom"]) ?? number(layout[horizontal ? "marginHorizontal" : "marginVertical"]) ?? margin
86
+ return .init(basis: Float(basis), grow: Float(grow), shrink: Float(shrink), min: minimum.map(Float.init) ?? .nan, max: maximum.map(Float.init) ?? .nan, start: Float(start), end: Float(end))
72
87
  }
73
88
  let frames = CraftMacFlex.layout(items, available: horizontal ? inner.width : inner.height, gap: gap, justify: style["justifyContent"] as? String ?? "flex-start")
74
89
  for (index, box) in visible.enumerated() {
75
90
  let crossAvailable = horizontal ? inner.height : inner.width
76
- let crossExplicit = dimension(box.style[horizontal ? "height" : "width"], base: crossAvailable)
77
- let alignment = box.style["alignSelf"] as? String ?? style["alignItems"] as? String ?? "stretch"
78
- let cross = min(crossAvailable, crossExplicit ?? (alignment == "stretch" ? crossAvailable : (horizontal ? sizes[index].height : sizes[index].width)))
91
+ let crossExplicit = dimension(box.layoutStyle[horizontal ? "height" : "width"], base: crossAvailable)
92
+ let alignment = box.layoutStyle["alignSelf"] as? String ?? style["alignItems"] as? String ?? "stretch"
93
+ let measured = horizontal ? box.measure(width: frames[index].0, stretch: true) : sizes[index]
94
+ let cross = min(crossAvailable, crossExplicit ?? (alignment == "stretch" ? crossAvailable : (horizontal ? measured.height : measured.width)))
79
95
  let offset = alignment == "center" ? (crossAvailable - cross) / 2 : alignment == "flex-end" ? crossAvailable - cross : 0
80
96
  box.frame = horizontal ? CGRect(x: left + frames[index].1, y: top + offset, width: frames[index].0, height: cross)
81
97
  : CGRect(x: left + offset, y: top + frames[index].1, width: cross, height: frames[index].0)
82
98
  box.layoutSubtreeIfNeeded()
83
99
  }
100
+ for box in boxes where !box.isHidden && box.layoutStyle["position"] as? String == "absolute" {
101
+ let measured = box.measure(width: size.width)
102
+ let leading = dimension(box.layoutStyle["left"], base: size.width), trailing = dimension(box.layoutStyle["right"], base: size.width)
103
+ let above = dimension(box.layoutStyle["top"], base: size.height), below = dimension(box.layoutStyle["bottom"], base: size.height)
104
+ let width = dimension(box.layoutStyle["width"], base: size.width) ?? (leading != nil && trailing != nil ? max(0, size.width - leading! - trailing!) : measured.width)
105
+ let height = dimension(box.layoutStyle["height"], base: size.height) ?? (above != nil && below != nil ? max(0, size.height - above! - below!) : measured.height)
106
+ box.frame = CGRect(x: (leading ?? (trailing.map { size.width - $0 - width } ?? 0)), y: (above ?? (below.map { size.height - $0 - height } ?? 0)), width: width, height: height)
107
+ box.layoutSubtreeIfNeeded()
108
+ }
84
109
  }
85
- func fitting(width: CGFloat) -> CGSize {
110
+ func fitting(width: CGFloat, stretchChildren: Bool = true) -> CGSize {
86
111
  let padding = number(style["padding"]) ?? 0
87
112
  let horizontalPadding = (number(style["paddingLeft"]) ?? number(style["paddingHorizontal"]) ?? padding) + (number(style["paddingRight"]) ?? number(style["paddingHorizontal"]) ?? padding)
88
113
  let verticalPadding = (number(style["paddingTop"]) ?? number(style["paddingVertical"]) ?? padding) + (number(style["paddingBottom"]) ?? number(style["paddingVertical"]) ?? padding)
89
- let visible = boxes.filter { !$0.isHidden && $0.kind != "Modal" }
90
- let sizes = visible.map { $0.measure(width: max(0, width - horizontalPadding)) }
114
+ let visible = boxes.filter { !$0.isHidden && $0.kind != "Modal" && $0.layoutStyle["position"] as? String != "absolute" }
115
+ let available = max(0, width - horizontalPadding)
116
+ var sizes = visible.map { box in
117
+ let alignment = box.layoutStyle["alignSelf"] as? String ?? style["alignItems"] as? String ?? "stretch"
118
+ return box.measure(width: available, stretch: stretchChildren && style["flexDirection"] as? String != "row" && alignment == "stretch")
119
+ }
91
120
  let gaps = CGFloat(max(0, sizes.count - 1)) * (number(style["gap"]) ?? 0)
92
121
  if style["flexDirection"] as? String == "row" {
122
+ let items = visible.enumerated().map { index, box -> CraftMacFlex.Item in
123
+ let layout = box.layoutStyle
124
+ let grow = number(layout["flexGrow"]) ?? number(layout["flex"]) ?? 0
125
+ let basis = dimension(layout["flexBasis"], base: available) ?? (number(layout["flex"]) != nil ? 0 : sizes[index].width)
126
+ return .init(basis: Float(basis), grow: Float(grow), shrink: Float(number(layout["flexShrink"]) ?? (grow > 0 ? 1 : 0)), min: dimension(layout["minWidth"], base: available).map(Float.init) ?? .nan, max: dimension(layout["maxWidth"], base: available).map(Float.init) ?? .nan, start: 0, end: 0)
127
+ }
128
+ let frames = CraftMacFlex.layout(items, available: available, gap: number(style["gap"]) ?? 0, justify: "flex-start")
129
+ sizes = visible.enumerated().map { index, box in box.measure(width: frames[index].0, stretch: stretchChildren) }
93
130
  return CGSize(width: sizes.reduce(0) { $0 + $1.width } + horizontalPadding + gaps, height: (sizes.map(\.height).max() ?? 0) + verticalPadding)
94
131
  }
95
- return CGSize(width: (sizes.map(\.width).max() ?? 0) + horizontalPadding, height: sizes.reduce(0) { $0 + $1.height } + verticalPadding + gaps)
132
+ let margins = visible.reduce(CGFloat(0)) { total, box in
133
+ let layout = box.layoutStyle
134
+ let margin = number(layout["marginVertical"]) ?? number(layout["margin"]) ?? 0
135
+ return total + (number(layout["marginTop"]) ?? margin) + (number(layout["marginBottom"]) ?? margin)
136
+ }
137
+ return CGSize(width: (sizes.map(\.width).max() ?? 0) + horizontalPadding, height: sizes.reduce(0) { $0 + $1.height } + verticalPadding + gaps + margins)
96
138
  }
97
139
  }
98
140
 
@@ -134,12 +176,23 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate {
134
176
  var imageSource = ""
135
177
  var scrollKey = ""
136
178
  var modal: NSPanel?
179
+ var material: NSVisualEffectView?
180
+ var edgeBorders: [String: CALayer] = [:]
181
+ var componentRoot: CraftMacBox? { props["data-stx-component-wrapper"] != nil && flow?.boxes.count == 1 && flow?.boxes.first?.kind != "Modal" ? flow?.boxes.first : nil }
182
+ var layoutStyle: [String: Any] {
183
+ guard let child = componentRoot else { return style }
184
+ let keys = ["width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight", "flex", "flexGrow", "flexShrink", "flexBasis", "alignSelf", "position", "left", "right", "top", "bottom", "margin", "marginTop", "marginRight", "marginBottom", "marginLeft", "marginHorizontal", "marginVertical"]
185
+ var result = style
186
+ for key in keys where result[key] == nil { result[key] = child.layoutStyle[key] }
187
+ return result
188
+ }
137
189
  override var isFlipped: Bool { true }
138
190
 
139
191
  init(id: String, kind: String, renderer: CraftMacRenderer) {
140
192
  self.id = id; self.kind = kind; self.renderer = renderer
141
193
  super.init(frame: .zero)
142
194
  wantsLayer = true
195
+ clipsToBounds = true
143
196
  setAccessibilityElement(false)
144
197
  switch kind {
145
198
  case "Text":
@@ -171,12 +224,13 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate {
171
224
  let text = NSTextField(labelWithString: "Unsupported native control: \(kind)"); control = text
172
225
  renderer.runtime.errors.append("Unsupported native control: \(kind)")
173
226
  }
174
- if let control { addSubview(control) }
227
+ if let control { control.clipsToBounds = true; addSubview(control) }
175
228
  }
176
229
  required init?(coder: NSCoder) { fatalError("Native controls are created from STX") }
177
230
 
178
231
  func configure(_ node: [String: Any]) {
179
232
  props = node["props"] as? [String: Any] ?? [:]
233
+ flow?.componentScope = props["data-stx-component-wrapper"] != nil
180
234
  style = node["style"] as? [String: Any] ?? [:]
181
235
  events = node["events"] as? [String: String] ?? [:]
182
236
  isHidden = style["display"] as? String == "none" || props["hidden"] as? Bool == true
@@ -186,6 +240,22 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate {
186
240
  }
187
241
  layer?.cornerRadius = number(style["borderRadius"]) ?? 0
188
242
  layer?.borderWidth = number(style["borderWidth"]) ?? 0
243
+ // AppKit views may draw their entire visibleRect, which can extend
244
+ // through adjacent panes on newer macOS releases. Native controls
245
+ // stay inside their authored boxes unless overflow is explicit.
246
+ clipsToBounds = style["overflow"] as? String != "visible"
247
+ if let container = control as? CraftMacFlow { container.clipsToBounds = clipsToBounds }
248
+ layer?.masksToBounds = clipsToBounds || (number(style["borderRadius"]) ?? 0) > 0
249
+ for edge in ["Top", "Right", "Bottom", "Left"] {
250
+ let border = edgeBorders[edge] ?? CALayer()
251
+ if edgeBorders[edge] == nil { edgeBorders[edge] = border; layer?.addSublayer(border) }
252
+ border.backgroundColor = CraftMacColor.parse(style["borderColor"], fallback: .separatorColor).cgColor
253
+ border.isHidden = (number(style["border\(edge)Width"]) ?? 0) <= 0
254
+ }
255
+ if let name = props["material"] as? String {
256
+ if material == nil { let effect = NSVisualEffectView(); effect.blendingMode = .behindWindow; effect.state = .followsWindowActiveState; addSubview(effect, positioned: .below, relativeTo: control); material = effect }
257
+ material?.material = name == "sidebar" ? .sidebar : .headerView
258
+ } else { material?.removeFromSuperview(); material = nil }
189
259
  alphaValue = number(style["opacity"]) ?? 1
190
260
  let identifier = props["testID"] as? String ?? props["testId"] as? String ?? props["accessibilityIdentifier"] as? String ?? id
191
261
  control?.setAccessibilityIdentifier(identifier)
@@ -204,12 +274,18 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate {
204
274
  text.lineBreakMode = text.maximumNumberOfLines == 1 ? .byTruncatingTail : .byWordWrapping
205
275
  }
206
276
  if let button = control as? NSButton {
207
- button.title = props["title"] as? String ?? nativeText(node)
277
+ let title = props["title"] as? String ?? nativeText(node)
278
+ button.title = props["iconOnly"] as? Bool == true ? "" : title
279
+ button.setAccessibilityLabel(label ?? title)
208
280
  button.font = font
281
+ button.alignment = style["textAlign"] as? String == "left" ? .left : style["textAlign"] as? String == "right" ? .right : .center
209
282
  button.isEnabled = props["disabled"] as? Bool != true
210
283
  if kind == "Switch" || kind == "Checkbox" { button.state = (props["value"] as? Bool ?? props["checked"] as? Bool ?? false) ? .on : .off }
211
284
  button.bezelStyle = props["variant"] as? String == "toolbar" ? .texturedRounded : .rounded
285
+ button.isBordered = !["toolbar", "ghost"].contains(props["variant"] as? String ?? "")
286
+ button.contentTintColor = CraftMacColor.parse(style["color"], fallback: .controlAccentColor)
212
287
  if let symbol = props["symbol"] as? String { button.image = NSImage(systemSymbolName: symbol, accessibilityDescription: button.title); button.imagePosition = button.title.isEmpty ? .imageOnly : .imageLeading }
288
+ if button.image != nil { button.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: number(style["fontSize"]) ?? 16, weight: .regular) }
213
289
  if props["variant"] as? String == "primary" { button.bezelColor = .controlAccentColor }
214
290
  if props["variant"] as? String == "danger" { button.contentTintColor = .systemRed }
215
291
  }
@@ -261,6 +337,12 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate {
261
337
  }
262
338
  let value = props["value"] as? String ?? ""
263
339
  if let text = (control as? NSScrollView)?.documentView as? NSTextView {
340
+ if let scroll = control as? NSScrollView {
341
+ let bordered = props["bordered"] as? Bool ?? true
342
+ scroll.borderType = bordered ? .bezelBorder : .noBorder
343
+ scroll.drawsBackground = bordered
344
+ text.drawsBackground = bordered
345
+ }
264
346
  if text.string != value { text.string = value }
265
347
  text.isEditable = props["disabled"] as? Bool != true; text.font = font
266
348
  if let editor = text as? CraftMacTextView { editor.placeholder = props["placeholder"] as? String ?? ""; editor.needsDisplay = true }
@@ -270,6 +352,7 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate {
270
352
  if field.stringValue != value { field.stringValue = value }
271
353
  field.placeholderString = props["placeholder"] as? String
272
354
  field.font = font; field.isEnabled = props["disabled"] as? Bool != true
355
+ if let bordered = props["bordered"] as? Bool { field.isBordered = bordered; field.drawsBackground = bordered }
273
356
  field.setAccessibilityIdentifier(props["testID"] as? String ?? props["testId"] as? String ?? id)
274
357
  }
275
358
  }
@@ -289,26 +372,35 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate {
289
372
  }; imageTask?.resume()
290
373
  }
291
374
 
292
- func measure(width: CGFloat) -> CGSize {
375
+ var textPadding: NSEdgeInsets {
376
+ let all = number(style["padding"]) ?? 0
377
+ let horizontal = number(style["paddingHorizontal"]) ?? all
378
+ let vertical = number(style["paddingVertical"]) ?? all
379
+ return NSEdgeInsets(top: number(style["paddingTop"]) ?? vertical, left: number(style["paddingLeft"]) ?? horizontal, bottom: number(style["paddingBottom"]) ?? vertical, right: number(style["paddingRight"]) ?? horizontal)
380
+ }
381
+ func measure(width: CGFloat, stretch: Bool = false) -> CGSize {
293
382
  if isHidden { return .zero }
383
+ if let child = componentRoot { return child.measure(width: width, stretch: stretch) }
294
384
  let specifiedWidth = dimension(style["width"], base: width)
295
385
  let limit = min(specifiedWidth ?? width, dimension(style["maxWidth"], base: width) ?? width)
296
386
  var size = CGSize(width: limit, height: 0)
297
387
  if let text = control as? NSTextField, kind == "Text" {
298
- let rect = (text.stringValue as NSString).boundingRect(with: CGSize(width: max(1, limit - 6), height: 1_000_000), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: text.font ?? NSFont.systemFont(ofSize: 13)])
299
- size = CGSize(width: min(limit, ceil(rect.width) + 6), height: max(18, ceil(rect.height) + 2))
388
+ let rect = (text.stringValue as NSString).boundingRect(with: CGSize(width: max(1, limit - 6 - textPadding.left - textPadding.right), height: 1_000_000), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [.font: text.font ?? NSFont.systemFont(ofSize: 13)])
389
+ size = CGSize(width: min(limit, ceil(rect.width) + 6 + textPadding.left + textPadding.right), height: max(18, ceil(rect.height) + 2))
390
+ if text.maximumNumberOfLines > 0 { size.height = min(size.height, ceil((text.font?.ascender ?? 13) - (text.font?.descender ?? -3) + (text.font?.leading ?? 0)) * CGFloat(text.maximumNumberOfLines) + 2) }
300
391
  if text.maximumNumberOfLines == 1 { size.height = max(18, (text.font?.pointSize ?? 13) + 5) }
392
+ size.height += textPadding.top + textPadding.bottom
301
393
  } else if kind == "ScrollView" || kind == "FlatList" || kind == "Sidebar" { size = CGSize(width: limit, height: 100) }
302
394
  else if kind == "Icon" { size = CGSize(width: 18, height: 18) }
303
395
  else if kind == "Image" || kind == "Video" { size = CGSize(width: limit, height: 160) }
304
396
  else if kind == "TextInput" { size = CGSize(width: limit, height: props["multiline"] as? Bool == true ? 48 : 28) }
305
397
  else if let control = control as? NSControl { size = control.intrinsicContentSize; size.width = max(24, size.width); size.height = max(26, size.height) }
306
- else if let flow { size = flow.fitting(width: limit) }
307
- size.width = specifiedWidth ?? min(limit, size.width)
398
+ else if let flow { size = flow.fitting(width: limit, stretchChildren: false) }
399
+ size.width = specifiedWidth ?? (flow != nil && stretch ? limit : min(limit, size.width))
308
400
  if let maximum = dimension(style["maxWidth"], base: width) { size.width = min(size.width, maximum) }
309
401
  // A naturally sized column can be narrower than its measurement limit.
310
402
  // Measure its wrapped children again at the width it will actually get.
311
- if let flow, style["flexDirection"] as? String != "row" {
403
+ if let flow, !(control is NSScrollView), style["flexDirection"] as? String != "row" {
312
404
  size.height = flow.fitting(width: size.width).height
313
405
  }
314
406
  size.height = dimension(style["height"], base: 1000) ?? size.height
@@ -318,9 +410,27 @@ final class CraftMacBox: NSView, NSTextFieldDelegate, NSTextViewDelegate {
318
410
  return size
319
411
  }
320
412
  override func layout() {
413
+ CATransaction.begin(); CATransaction.setDisableActions(true)
414
+ defer { CATransaction.commit() }
321
415
  super.layout()
322
- control?.frame = bounds
323
- if let scroll = control as? NSScrollView {
416
+ layer?.cornerRadius = min(number(style["borderRadius"]) ?? 0, min(bounds.width, bounds.height) / 2)
417
+ if kind == "Text" {
418
+ let padding = textPadding
419
+ control?.frame = CGRect(x: padding.left, y: padding.top, width: max(0, bounds.width - padding.left - padding.right), height: max(0, bounds.height - padding.top - padding.bottom))
420
+ } else { control?.frame = bounds }
421
+ material?.frame = bounds
422
+ for (edge, border) in edgeBorders {
423
+ let thickness = number(style["border\(edge)Width"]) ?? 0
424
+ switch edge {
425
+ case "Top": border.frame = CGRect(x: 0, y: 0, width: bounds.width, height: thickness)
426
+ case "Bottom": border.frame = CGRect(x: 0, y: bounds.height - thickness, width: bounds.width, height: thickness)
427
+ case "Left": border.frame = CGRect(x: 0, y: 0, width: thickness, height: bounds.height)
428
+ default: border.frame = CGRect(x: bounds.width - thickness, y: 0, width: thickness, height: bounds.height)
429
+ }
430
+ }
431
+ if let child = componentRoot {
432
+ child.frame = bounds; child.layoutSubtreeIfNeeded()
433
+ } else if let scroll = control as? NSScrollView {
324
434
  if let flow {
325
435
  let width = scroll.contentSize.width
326
436
  flow.frame = CGRect(x: 0, y: 0, width: width, height: max(scroll.contentSize.height, flow.fitting(width: width).height))
@@ -364,8 +474,8 @@ final class CraftMacSidebarRow: NSTableRowView {
364
474
  }
365
475
 
366
476
  final class CraftMacSidebar: NSScrollView, NSOutlineViewDataSource, NSOutlineViewDelegate {
367
- final class Item { let id: String; let label: String; let icon: String; let badge: String; let children: [Item]
368
- init(_ raw: [String: Any]) { id = raw["id"] as? String ?? ""; label = raw["label"] as? String ?? raw["header"] as? String ?? ""; icon = raw["icon"] as? String ?? ""; badge = raw["badge"].map { $0 is NSNull ? "" : String(describing: $0) } ?? ""; children = (raw["items"] as? [[String: Any]] ?? []).map(Item.init) }
477
+ final class Item { let id: String; let label: String; let icon: String; let badge: String; let color: NSColor; let children: [Item]
478
+ init(_ raw: [String: Any]) { id = raw["id"] as? String ?? ""; label = raw["label"] as? String ?? raw["header"] as? String ?? ""; icon = raw["icon"] as? String ?? ""; badge = raw["badge"].map { $0 is NSNull ? "" : String(describing: $0) } ?? ""; color = CraftMacColor.parse(raw["color"], fallback: .controlAccentColor); children = (raw["items"] as? [[String: Any]] ?? []).map(Item.init) }
369
479
  }
370
480
  weak var box: CraftMacBox?
371
481
  let outline = NSOutlineView()
@@ -375,7 +485,7 @@ final class CraftMacSidebar: NSScrollView, NSOutlineViewDataSource, NSOutlineVie
375
485
  self.box = box; super.init(frame: .zero)
376
486
  hasVerticalScroller = true; drawsBackground = false
377
487
  let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("label")); outline.addTableColumn(column); outline.outlineTableColumn = column
378
- outline.headerView = nil; outline.style = .plain; outline.backgroundColor = .windowBackgroundColor
488
+ outline.headerView = nil; outline.style = .plain; outline.backgroundColor = .clear
379
489
  outline.rowSizeStyle = .default; outline.indentationPerLevel = 0
380
490
  outline.dataSource = self; outline.delegate = self; outline.autoresizesOutlineColumn = true; documentView = outline
381
491
  }
@@ -402,12 +512,14 @@ final class CraftMacSidebar: NSScrollView, NSOutlineViewDataSource, NSOutlineVie
402
512
  }
403
513
  func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
404
514
  let item = item as! Item
405
- let cell = NSTableCellView(); let label = NSTextField(labelWithString: item.label + (item.badge.isEmpty ? "" : " \(item.badge)"))
515
+ let cell = NSTableCellView(); let label = NSTextField(labelWithString: item.label)
406
516
  label.font = NSFont.systemFont(ofSize: 13); label.lineBreakMode = .byTruncatingTail; cell.textField = label; cell.addSubview(label)
407
517
  if !item.children.isEmpty { label.font = NSFont.systemFont(ofSize: 11, weight: .semibold); label.textColor = .secondaryLabelColor }
408
518
  label.translatesAutoresizingMaskIntoConstraints = false
409
- NSLayoutConstraint.activate([label.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: item.icon.isEmpty ? 4 : 27), label.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -6), label.centerYAnchor.constraint(equalTo: cell.centerYAnchor)])
410
- if !item.icon.isEmpty { let image = NSImageView(image: NSImage(systemSymbolName: item.icon, accessibilityDescription: nil) ?? NSImage()); image.frame = CGRect(x: 4, y: 7, width: 18, height: 18); cell.imageView = image; cell.addSubview(image) }
519
+ let badge = NSTextField(labelWithString: item.badge); badge.font = NSFont.systemFont(ofSize: 11); badge.textColor = .secondaryLabelColor
520
+ badge.translatesAutoresizingMaskIntoConstraints = false; badge.setContentHuggingPriority(.required, for: .horizontal); cell.addSubview(badge)
521
+ NSLayoutConstraint.activate([label.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: item.icon.isEmpty ? 16 : 39), label.trailingAnchor.constraint(lessThanOrEqualTo: badge.leadingAnchor, constant: -8), label.centerYAnchor.constraint(equalTo: cell.centerYAnchor), badge.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: -16), badge.centerYAnchor.constraint(equalTo: cell.centerYAnchor)])
522
+ if !item.icon.isEmpty { let image = NSImageView(image: NSImage(systemSymbolName: item.icon, accessibilityDescription: nil) ?? NSImage()); image.contentTintColor = item.color; image.frame = CGRect(x: 16, y: 8, width: 16, height: 16); cell.imageView = image; cell.addSubview(image) }
411
523
  cell.setAccessibilityIdentifier(item.id); cell.setAccessibilityLabel(item.label)
412
524
  return cell
413
525
  }
@@ -426,6 +538,8 @@ final class CraftMacRenderer {
426
538
  var sheets: [String: NSPanel] = [:]
427
539
  init(runtime: CraftMacRuntime) { self.runtime = runtime }
428
540
  func render(_ document: [String: Any]) {
541
+ CATransaction.begin(); CATransaction.setDisableActions(true)
542
+ defer { CATransaction.commit() }
429
543
  var retained = Set<String>()
430
544
  var scrolls: [(CraftMacBox, CGPoint, Bool)] = []
431
545
  for box in boxes.values { if let scroll = box.control as? NSScrollView, box.kind != "Sidebar" {
@@ -473,7 +587,8 @@ final class CraftMacRenderer {
473
587
  guard let scroll = box.control as? NSScrollView, box.kind != "TextInput" else { continue }
474
588
  let key = box.props["scrollKey"] as? String ?? ""
475
589
  let changed = key != box.scrollKey; box.scrollKey = key
476
- let y = (bottom || changed) && box.props["autoScrollBottom"] as? Bool == true ? max(0, (scroll.documentView?.frame.height ?? 0) - scroll.contentSize.height) : origin.y
590
+ let maximum = max(0, (scroll.documentView?.frame.height ?? 0) - scroll.contentSize.height)
591
+ let y = (bottom || changed) && box.props["autoScrollBottom"] as? Bool == true ? maximum : min(maximum, max(0, origin.y))
477
592
  scroll.contentView.scroll(to: CGPoint(x: origin.x, y: y)); scroll.reflectScrolledClipView(scroll.contentView)
478
593
  }
479
594
  root.needsLayout = true
package/dist/types.d.ts CHANGED
@@ -558,6 +558,8 @@ export interface AppConfig {
558
558
  * Path to Craft binary (auto-detected if not provided)
559
559
  */
560
560
  craftPath?: string;
561
+ /** macOS webview apps may omit the separate headless scripting engine. */
562
+ runtimeProfile?: 'full' | 'webview';
561
563
  /**
562
564
  * Suppress all non-error output from the native binary.
563
565
  * When true, the binary runs with --quiet and stdio is piped.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.346",
3
+ "version": "0.0.350",
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>",