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.
- package/dist/binary-resolver.d.ts +1 -1
- package/dist/cli.js +17 -4
- package/dist/index.cjs +18 -5
- package/dist/index.js +18 -5
- package/dist/macos/templates/CraftMacApplication.swift +26 -3
- package/dist/macos/templates/CraftMacRenderer.swift +147 -32
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
|
67
|
-
let grow = number(
|
|
68
|
-
let
|
|
69
|
-
let
|
|
70
|
-
let
|
|
71
|
-
|
|
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.
|
|
77
|
-
let alignment = box.
|
|
78
|
-
let
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
if
|
|
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 = .
|
|
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
|
|
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
|
-
|
|
410
|
-
|
|
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
|
|
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.
|