craft-native 0.0.330 → 0.0.340

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -126,7 +126,7 @@ var init_temp_dir = __esm(() => {
126
126
  });
127
127
 
128
128
  // src/package.ts
129
- import { spawn } from "child_process";
129
+ import { spawn, spawnSync } from "child_process";
130
130
  import { createHash } from "crypto";
131
131
  import {
132
132
  chmodSync,
@@ -144,7 +144,7 @@ import {
144
144
  writeFileSync as writeFileSync3
145
145
  } from "fs";
146
146
  import { homedir } from "os";
147
- import { basename, join as join2, resolve } from "path";
147
+ import { basename, dirname, join as join2, resolve } from "path";
148
148
  import { deflateRawSync } from "zlib";
149
149
  function crc32(data) {
150
150
  let c = 4294967295;
@@ -241,6 +241,19 @@ async function packageApp(config) {
241
241
  }
242
242
  return results;
243
243
  }
244
+ function macOSAppStoreRuntimePath(providerPath) {
245
+ const runtime = basename(providerPath) === "craft-app-store" ? providerPath : join2(dirname(providerPath), "craft-app-store");
246
+ if (!existsSync4(runtime))
247
+ throw new Error("App Store packaging requires Pantry\u2019s craft-app-store runtime. Upgrade Craft with Pantry before building.");
248
+ const bytes = readFileSync2(runtime);
249
+ const privateMarkers = ["MenuBarClientCore", "MBAssessmentMode", "developerExtrasEnabled", "javaScriptCanAccessClipboard", "drawsBackground"];
250
+ if (privateMarkers.some((marker) => bytes.includes(Buffer.from(marker))))
251
+ throw new Error("App Store packaging refused a runtime that includes private platform APIs.");
252
+ const probe = spawnSync(runtime, ["--distribution"], { encoding: "utf8", timeout: 1e4 });
253
+ if (probe.status !== 0 || probe.stdout.trim() !== "app-store")
254
+ throw new Error("App Store packaging refused a runtime that includes private platform APIs.");
255
+ return runtime;
256
+ }
244
257
  async function packageMacOS(config, outDir) {
245
258
  const results = [];
246
259
  const { name, version, bundleId = `com.myapp.${name.toLowerCase()}` } = config;
@@ -248,6 +261,14 @@ async function packageMacOS(config, outDir) {
248
261
  const hardenedRuntime = !opts.appStore;
249
262
  if (opts.appStore && (!opts.entitlements || !opts.provisioningProfile))
250
263
  return [{ success: false, platform: "macos", format: "app", error: "App Store packaging requires macos.entitlements and macos.provisioningProfile" }];
264
+ let nativeRuntimePath = opts.nativeRuntimePath;
265
+ if (opts.appStore && nativeRuntimePath) {
266
+ try {
267
+ nativeRuntimePath = macOSAppStoreRuntimePath(nativeRuntimePath);
268
+ } catch (error) {
269
+ return [{ success: false, platform: "macos", format: "app", error: error.message }];
270
+ }
271
+ }
251
272
  const appBundlePath = join2(outDir, `${name}.app`);
252
273
  const appBundle = opts.appBundlePath ? copyMacOSAppBundle(opts.appBundlePath, appBundlePath) : createMacOSAppBundle({
253
274
  name,
@@ -262,7 +283,9 @@ async function packageMacOS(config, outDir) {
262
283
  iconPath: config.iconPath,
263
284
  provisioningProfile: opts.provisioningProfile,
264
285
  additionalExecutables: opts.additionalExecutables,
265
- nativeRuntimePath: opts.nativeRuntimePath,
286
+ nativeRuntimePath,
287
+ nativeHost: opts.appStore === true && Boolean(nativeRuntimePath),
288
+ nativeWindow: opts.nativeWindow,
266
289
  outputPath: appBundlePath
267
290
  });
268
291
  if (!appBundle.success) {
@@ -715,7 +738,7 @@ function createMacOSAppBundle(opts) {
715
738
  const macOS = join2(contents, "MacOS");
716
739
  mkdirSync(macOS, { recursive: true });
717
740
  mkdirSync(join2(contents, "Resources"), { recursive: true });
718
- copyFileSync(binaryPath, join2(macOS, name));
741
+ copyFileSync(opts.nativeHost ? opts.nativeRuntimePath : binaryPath, join2(macOS, name));
719
742
  chmodSync(join2(macOS, name), 493);
720
743
  for (const executable of additionalExecutables || []) {
721
744
  if (!existsSync4(executable))
@@ -724,7 +747,18 @@ function createMacOSAppBundle(opts) {
724
747
  copyFileSync(executable, destination);
725
748
  chmodSync(destination, 493);
726
749
  }
727
- if (opts.nativeRuntimePath) {
750
+ if (opts.nativeHost) {
751
+ const backend = join2(macOS, "craft-backend");
752
+ copyFileSync(binaryPath, backend);
753
+ chmodSync(backend, 493);
754
+ writeFileSync3(join2(contents, "Resources", "craft.json"), `${JSON.stringify({
755
+ title: name,
756
+ appName: name,
757
+ ...opts.nativeWindow,
758
+ backend: { executable: "craft-backend" }
759
+ }, null, 2)}
760
+ `);
761
+ } else if (opts.nativeRuntimePath) {
728
762
  const helper = macOSRuntimeExecutablePath(outputPath);
729
763
  const helperContents = resolve(helper, "..", "..");
730
764
  mkdirSync(join2(helperContents, "MacOS"), { recursive: true });
@@ -2988,7 +3022,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
2988
3022
  `);
2989
3023
  }
2990
3024
  // package.json
2991
- var version = "0.0.330";
3025
+ var version = "0.0.340";
2992
3026
 
2993
3027
  // bin/cli.ts
2994
3028
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -375,7 +375,7 @@ function craftBinaryNotFoundMessage(triedPath) {
375
375
  `);
376
376
  }
377
377
  // package.json
378
- var version = "0.0.330";
378
+ var version = "0.0.340";
379
379
 
380
380
  // src/sdk-version.ts
381
381
  var SDK_VERSION = version;
@@ -607,6 +607,19 @@ async function packageApp(config) {
607
607
  }
608
608
  return results;
609
609
  }
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");
612
+ if (!import_fs.existsSync(runtime))
613
+ throw new Error("App Store packaging requires Pantry’s craft-app-store runtime. Upgrade Craft with Pantry before building.");
614
+ const bytes = import_fs.readFileSync(runtime);
615
+ const privateMarkers = ["MenuBarClientCore", "MBAssessmentMode", "developerExtrasEnabled", "javaScriptCanAccessClipboard", "drawsBackground"];
616
+ if (privateMarkers.some((marker) => bytes.includes(Buffer.from(marker))))
617
+ throw new Error("App Store packaging refused a runtime that includes private platform APIs.");
618
+ const probe = import_child_process.spawnSync(runtime, ["--distribution"], { encoding: "utf8", timeout: 1e4 });
619
+ if (probe.status !== 0 || probe.stdout.trim() !== "app-store")
620
+ throw new Error("App Store packaging refused a runtime that includes private platform APIs.");
621
+ return runtime;
622
+ }
610
623
  async function packageMacOS(config, outDir) {
611
624
  const results = [];
612
625
  const { name, version, bundleId = `com.myapp.${name.toLowerCase()}` } = config;
@@ -614,6 +627,14 @@ async function packageMacOS(config, outDir) {
614
627
  const hardenedRuntime = !opts.appStore;
615
628
  if (opts.appStore && (!opts.entitlements || !opts.provisioningProfile))
616
629
  return [{ success: false, platform: "macos", format: "app", error: "App Store packaging requires macos.entitlements and macos.provisioningProfile" }];
630
+ let nativeRuntimePath = opts.nativeRuntimePath;
631
+ if (opts.appStore && nativeRuntimePath) {
632
+ try {
633
+ nativeRuntimePath = macOSAppStoreRuntimePath(nativeRuntimePath);
634
+ } catch (error) {
635
+ return [{ success: false, platform: "macos", format: "app", error: error.message }];
636
+ }
637
+ }
617
638
  const appBundlePath = import_path.join(outDir, `${name}.app`);
618
639
  const appBundle = opts.appBundlePath ? copyMacOSAppBundle(opts.appBundlePath, appBundlePath) : createMacOSAppBundle({
619
640
  name,
@@ -628,7 +649,9 @@ async function packageMacOS(config, outDir) {
628
649
  iconPath: config.iconPath,
629
650
  provisioningProfile: opts.provisioningProfile,
630
651
  additionalExecutables: opts.additionalExecutables,
631
- nativeRuntimePath: opts.nativeRuntimePath,
652
+ nativeRuntimePath,
653
+ nativeHost: opts.appStore === true && Boolean(nativeRuntimePath),
654
+ nativeWindow: opts.nativeWindow,
632
655
  outputPath: appBundlePath
633
656
  });
634
657
  if (!appBundle.success) {
@@ -1082,7 +1105,7 @@ function createMacOSAppBundle(opts) {
1082
1105
  const macOS = import_path.join(contents, "MacOS");
1083
1106
  import_fs.mkdirSync(macOS, { recursive: true });
1084
1107
  import_fs.mkdirSync(import_path.join(contents, "Resources"), { recursive: true });
1085
- import_fs.copyFileSync(binaryPath, import_path.join(macOS, name));
1108
+ import_fs.copyFileSync(opts.nativeHost ? opts.nativeRuntimePath : binaryPath, import_path.join(macOS, name));
1086
1109
  import_fs.chmodSync(import_path.join(macOS, name), 493);
1087
1110
  for (const executable of additionalExecutables || []) {
1088
1111
  if (!import_fs.existsSync(executable))
@@ -1091,7 +1114,18 @@ function createMacOSAppBundle(opts) {
1091
1114
  import_fs.copyFileSync(executable, destination);
1092
1115
  import_fs.chmodSync(destination, 493);
1093
1116
  }
1094
- if (opts.nativeRuntimePath) {
1117
+ if (opts.nativeHost) {
1118
+ const backend = import_path.join(macOS, "craft-backend");
1119
+ import_fs.copyFileSync(binaryPath, backend);
1120
+ import_fs.chmodSync(backend, 493);
1121
+ import_fs.writeFileSync(import_path.join(contents, "Resources", "craft.json"), `${JSON.stringify({
1122
+ title: name,
1123
+ appName: name,
1124
+ ...opts.nativeWindow,
1125
+ backend: { executable: "craft-backend" }
1126
+ }, null, 2)}
1127
+ `);
1128
+ } else if (opts.nativeRuntimePath) {
1095
1129
  const helper = macOSRuntimeExecutablePath(outputPath);
1096
1130
  const helperContents = import_path.resolve(helper, "..", "..");
1097
1131
  import_fs.mkdirSync(import_path.join(helperContents, "MacOS"), { recursive: true });
package/dist/index.js CHANGED
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
117
117
  `);
118
118
  }
119
119
  // package.json
120
- var version = "0.0.330";
120
+ var version = "0.0.340";
121
121
 
122
122
  // src/sdk-version.ts
123
123
  var SDK_VERSION = version;
@@ -237,7 +237,7 @@ function removeTempDir(dir) {
237
237
  }
238
238
 
239
239
  // src/package.ts
240
- import { spawn } from "child_process";
240
+ import { spawn, spawnSync } from "child_process";
241
241
  import { createHash } from "crypto";
242
242
  import {
243
243
  chmodSync,
@@ -255,7 +255,7 @@ import {
255
255
  writeFileSync as writeFileSync2
256
256
  } from "fs";
257
257
  import { homedir } from "os";
258
- import { basename, join as join2, resolve } from "path";
258
+ import { basename, dirname, join as join2, resolve } from "path";
259
259
  import { deflateRawSync } from "zlib";
260
260
  var DEFAULT_DEB_DEPENDENCIES = ["libgtk-3-0", "libwebkit2gtk-4.1-37", "libnotify-bin"];
261
261
  var CRC32_TABLE = (() => {
@@ -363,6 +363,19 @@ async function packageApp(config) {
363
363
  }
364
364
  return results;
365
365
  }
366
+ function macOSAppStoreRuntimePath(providerPath) {
367
+ const runtime = basename(providerPath) === "craft-app-store" ? providerPath : join2(dirname(providerPath), "craft-app-store");
368
+ if (!existsSync3(runtime))
369
+ throw new Error("App Store packaging requires Pantry\u2019s craft-app-store runtime. Upgrade Craft with Pantry before building.");
370
+ const bytes = readFileSync(runtime);
371
+ const privateMarkers = ["MenuBarClientCore", "MBAssessmentMode", "developerExtrasEnabled", "javaScriptCanAccessClipboard", "drawsBackground"];
372
+ if (privateMarkers.some((marker) => bytes.includes(Buffer.from(marker))))
373
+ throw new Error("App Store packaging refused a runtime that includes private platform APIs.");
374
+ const probe = spawnSync(runtime, ["--distribution"], { encoding: "utf8", timeout: 1e4 });
375
+ if (probe.status !== 0 || probe.stdout.trim() !== "app-store")
376
+ throw new Error("App Store packaging refused a runtime that includes private platform APIs.");
377
+ return runtime;
378
+ }
366
379
  async function packageMacOS(config, outDir) {
367
380
  const results = [];
368
381
  const { name, version, bundleId = `com.myapp.${name.toLowerCase()}` } = config;
@@ -370,6 +383,14 @@ async function packageMacOS(config, outDir) {
370
383
  const hardenedRuntime = !opts.appStore;
371
384
  if (opts.appStore && (!opts.entitlements || !opts.provisioningProfile))
372
385
  return [{ success: false, platform: "macos", format: "app", error: "App Store packaging requires macos.entitlements and macos.provisioningProfile" }];
386
+ let nativeRuntimePath = opts.nativeRuntimePath;
387
+ if (opts.appStore && nativeRuntimePath) {
388
+ try {
389
+ nativeRuntimePath = macOSAppStoreRuntimePath(nativeRuntimePath);
390
+ } catch (error) {
391
+ return [{ success: false, platform: "macos", format: "app", error: error.message }];
392
+ }
393
+ }
373
394
  const appBundlePath = join2(outDir, `${name}.app`);
374
395
  const appBundle = opts.appBundlePath ? copyMacOSAppBundle(opts.appBundlePath, appBundlePath) : createMacOSAppBundle({
375
396
  name,
@@ -384,7 +405,9 @@ async function packageMacOS(config, outDir) {
384
405
  iconPath: config.iconPath,
385
406
  provisioningProfile: opts.provisioningProfile,
386
407
  additionalExecutables: opts.additionalExecutables,
387
- nativeRuntimePath: opts.nativeRuntimePath,
408
+ nativeRuntimePath,
409
+ nativeHost: opts.appStore === true && Boolean(nativeRuntimePath),
410
+ nativeWindow: opts.nativeWindow,
388
411
  outputPath: appBundlePath
389
412
  });
390
413
  if (!appBundle.success) {
@@ -838,7 +861,7 @@ function createMacOSAppBundle(opts) {
838
861
  const macOS = join2(contents, "MacOS");
839
862
  mkdirSync(macOS, { recursive: true });
840
863
  mkdirSync(join2(contents, "Resources"), { recursive: true });
841
- copyFileSync(binaryPath, join2(macOS, name));
864
+ copyFileSync(opts.nativeHost ? opts.nativeRuntimePath : binaryPath, join2(macOS, name));
842
865
  chmodSync(join2(macOS, name), 493);
843
866
  for (const executable of additionalExecutables || []) {
844
867
  if (!existsSync3(executable))
@@ -847,7 +870,18 @@ function createMacOSAppBundle(opts) {
847
870
  copyFileSync(executable, destination);
848
871
  chmodSync(destination, 493);
849
872
  }
850
- if (opts.nativeRuntimePath) {
873
+ if (opts.nativeHost) {
874
+ const backend = join2(macOS, "craft-backend");
875
+ copyFileSync(binaryPath, backend);
876
+ chmodSync(backend, 493);
877
+ writeFileSync2(join2(contents, "Resources", "craft.json"), `${JSON.stringify({
878
+ title: name,
879
+ appName: name,
880
+ ...opts.nativeWindow,
881
+ backend: { executable: "craft-backend" }
882
+ }, null, 2)}
883
+ `);
884
+ } else if (opts.nativeRuntimePath) {
851
885
  const helper = macOSRuntimeExecutablePath(outputPath);
852
886
  const helperContents = resolve(helper, "..", "..");
853
887
  mkdirSync(join2(helperContents, "MacOS"), { recursive: true });
@@ -10387,7 +10421,7 @@ var commonOptimizations = {
10387
10421
  };
10388
10422
  // src/updater/index.ts
10389
10423
  import { createReadStream, createWriteStream, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmdirSync, statSync as statSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
10390
- import { join as join4, basename as basename3, dirname as dirname2 } from "path";
10424
+ import { join as join4, basename as basename3, dirname as dirname3 } from "path";
10391
10425
  import { tmpdir as tmpdir2 } from "os";
10392
10426
  import { execFileSync as execFileSync2, spawn as spawn3 } from "child_process";
10393
10427
  import { createHash as createHash2 } from "crypto";
@@ -10396,7 +10430,7 @@ import { EventEmitter as EventEmitter3 } from "events";
10396
10430
  // src/updater/macos-bundle.ts
10397
10431
  import { execFile, execFileSync } from "child_process";
10398
10432
  import { accessSync, constants, existsSync as existsSync4, renameSync, rmSync as rmSync3, statSync as statSync2 } from "fs";
10399
- import { basename as basename2, dirname, join as join3 } from "path";
10433
+ import { basename as basename2, dirname as dirname2, join as join3 } from "path";
10400
10434
  import { promisify } from "util";
10401
10435
  var execFileAsync = promisify(execFile);
10402
10436
  function isMacOS() {
@@ -10549,7 +10583,7 @@ async function extractBundle(archivePath, stagingDir) {
10549
10583
  async function swapBundle(stagedPath, installedPath) {
10550
10584
  if (!existsSync4(stagedPath))
10551
10585
  throw new Error(`Staged bundle is missing: ${stagedPath}`);
10552
- const parent = dirname(installedPath);
10586
+ const parent = dirname2(installedPath);
10553
10587
  const stamp = `${Date.now()}.${process.pid}`;
10554
10588
  const incoming = join3(parent, `.${basename2(installedPath)}.${stamp}.incoming`);
10555
10589
  const retired = join3(parent, `.${basename2(installedPath)}.${stamp}.retired`);
@@ -10590,7 +10624,7 @@ async function swapBundle(stagedPath, installedPath) {
10590
10624
  }
10591
10625
  function canReplaceBundle(installedPath) {
10592
10626
  try {
10593
- const parent = dirname(installedPath);
10627
+ const parent = dirname2(installedPath);
10594
10628
  accessSync(parent, constants.W_OK);
10595
10629
  if (existsSync4(installedPath))
10596
10630
  statSync2(installedPath);
@@ -10911,7 +10945,7 @@ class AutoUpdater extends EventEmitter3 {
10911
10945
  return;
10912
10946
  }
10913
10947
  if (!canReplaceBundle(appPath)) {
10914
- throw new Error(`Cannot replace ${appPath}: ${dirname2(appPath)} is not writable by this user. ` + "Install the update manually, or move the app somewhere you own.");
10948
+ throw new Error(`Cannot replace ${appPath}: ${dirname3(appPath)} is not writable by this user. ` + "Install the update manually, or move the app somewhere you own.");
10915
10949
  }
10916
10950
  const staged = await extractBundle(downloadPath);
10917
10951
  try {
@@ -10957,7 +10991,7 @@ class AutoUpdater extends EventEmitter3 {
10957
10991
  stdio: "ignore"
10958
10992
  });
10959
10993
  } else if (downloadPath.endsWith(".zip")) {
10960
- const appDir = dirname2(this.config.appPath);
10994
+ const appDir = dirname3(this.config.appPath);
10961
10995
  execFileSync2("powershell", [
10962
10996
  "-NoProfile",
10963
10997
  "-NonInteractive",
@@ -10979,7 +11013,7 @@ class AutoUpdater extends EventEmitter3 {
10979
11013
  } else if (downloadPath.endsWith(".rpm")) {
10980
11014
  execFileSync2("sudo", ["rpm", "-U", downloadPath]);
10981
11015
  } else if (downloadPath.endsWith(".tar.gz")) {
10982
- const appDir = dirname2(this.config.appPath);
11016
+ const appDir = dirname3(this.config.appPath);
10983
11017
  execFileSync2("tar", ["-xzf", downloadPath, "-C", appDir]);
10984
11018
  }
10985
11019
  }
@@ -60,7 +60,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
60
60
  /// The row this cell showed is gone; its views stay in the cell, and
61
61
  /// the next row it shows reuses them when it has the same shape.
62
62
  func recycle() {
63
- if let identity = representedIdentity { onRecycle?(identity) }
63
+ if let identity = representedIdentity, showsHostedView { onRecycle?(identity) }
64
64
  representedIdentity = nil
65
65
  onRecycle = nil
66
66
  }
@@ -81,6 +81,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
81
81
  // since moved to another cell is that cell's now, and pulling it
82
82
  // from there blanked a row on screen.
83
83
  if let shown = hostedView, shown !== view, shown.superview === contentView { shown.removeFromSuperview() }
84
+ NSLayoutConstraint.deactivate(hostedConstraints)
84
85
  hostedView = view
85
86
  view.translatesAutoresizingMaskIntoConstraints = false
86
87
  contentView.addSubview(view)
@@ -110,6 +111,8 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
110
111
  func releaseHostedView() {
111
112
  recycle()
112
113
  if let shown = hostedView, shown.superview === contentView { shown.removeFromSuperview() }
114
+ NSLayoutConstraint.deactivate(hostedConstraints)
115
+ hostedConstraints = []
113
116
  hostedView = nil
114
117
  }
115
118
 
@@ -195,8 +198,11 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
195
198
  private var endReachedThreshold = 0.1
196
199
  private var endReachedSignature: String?
197
200
  private var dataContentSignature = ""
201
+ private var dataOrdinals: [Int] = []
202
+ private var dataItemCount = 0
198
203
  private var columns = 1
199
204
  private var isApplyingSnapshot = false
205
+ private var snapshotNeedsReset = false
200
206
  private var pendingApply: (() -> Void)?
201
207
  private var pendingThemeRefresh = false
202
208
  private let nativeRefreshControl = UIRefreshControl()
@@ -224,6 +230,10 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
224
230
  backgroundColor = .clear
225
231
  alwaysBounceVertical = true
226
232
  delegate = self
233
+ // The renderer keeps one live view per keyed row. UIKit's speculative
234
+ // cell prefetch can otherwise move that view out of its visible cell.
235
+ // UICollectionView still recycles and self-sizes the visible window.
236
+ isPrefetchingEnabled = false
227
237
  keyboardTapGesture.cancelsTouchesInView = false
228
238
  keyboardTapGesture.delegate = self
229
239
  keyboardTapGesture.addTarget(self, action: #selector(keyboardTap))
@@ -382,13 +392,15 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
382
392
  let padding = nonNegative(raw?["padding"]) ?? 0
383
393
  let horizontal = nonNegative(raw?["paddingHorizontal"]) ?? padding
384
394
  let vertical = nonNegative(raw?["paddingVertical"]) ?? padding
385
- contentInset = UIEdgeInsets(
395
+ let inset = UIEdgeInsets(
386
396
  top: nonNegative(raw?["paddingTop"]) ?? vertical,
387
397
  left: nonNegative(raw?["paddingLeft"]) ?? horizontal,
388
398
  bottom: nonNegative(raw?["paddingBottom"]) ?? vertical,
389
399
  right: nonNegative(raw?["paddingRight"]) ?? horizontal
390
400
  )
391
- scrollIndicatorInsets = contentInset
401
+ guard contentInset != inset else { return }
402
+ contentInset = inset
403
+ scrollIndicatorInsets = inset
392
404
  collectionViewLayout.invalidateLayout()
393
405
  }
394
406
 
@@ -479,7 +491,9 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
479
491
  self.removeItem = removeItem
480
492
  self.endReached = endReached
481
493
  self.endReachedThreshold = max(0, endReachedThreshold)
482
- self.columns = max(1, columns)
494
+ let nextColumns = max(1, columns)
495
+ let layoutChanged = self.columns != nextColumns || flowLayout.scrollDirection != (horizontal ? .horizontal : .vertical)
496
+ self.columns = nextColumns
483
497
 
484
498
  layoutIfNeeded()
485
499
  let oldItems = itemsByIdentity
@@ -496,6 +510,19 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
496
510
  return Item(identity: identity, node: node, signature: signature, isChrome: role != nil && role != "item")
497
511
  }
498
512
  if inverted { next.reverse() }
513
+ // Reapplying an identical keyed tree during a timer or badge update
514
+ // must not reconfigure cells, invalidate self-sizing, or reset a drag.
515
+ if !snapshotNeedsReset, !layoutChanged, next.count == items.count,
516
+ zip(next, items).allSatisfy({ $0.identity == $1.identity && $0.signature == $1.signature }) {
517
+ finishApplyingSnapshot()
518
+ return
519
+ }
520
+ snapshotNeedsReset = false
521
+ dataItemCount = 0
522
+ dataOrdinals = next.map { item in
523
+ if !item.isChrome { dataItemCount += 1 }
524
+ return dataItemCount - 1
525
+ }
499
526
  items = next
500
527
  itemsByIdentity = Dictionary(uniqueKeysWithValues: next.map { ($0.identity, $0) })
501
528
 
@@ -504,7 +531,7 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
504
531
  alwaysBounceVertical = !horizontal
505
532
  showsHorizontalScrollIndicator = horizontal
506
533
  showsVerticalScrollIndicator = !horizontal
507
- flowLayout.invalidateLayout()
534
+ if layoutChanged { flowLayout.invalidateLayout() }
508
535
 
509
536
  let nextIds = Set(itemsByIdentity.keys)
510
537
  for identity in oldItems.keys where !nextIds.contains(identity) { removeItem(identity) }
@@ -550,9 +577,12 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
550
577
  }
551
578
 
552
579
  func discardAll() {
580
+ snapshotNeedsReset = true
553
581
  visibleCells.compactMap { $0 as? Cell }.forEach { $0.releaseHostedView() }
554
582
  itemsByIdentity.keys.forEach { recycleItem?($0) }
555
583
  items = []
584
+ dataOrdinals = []
585
+ dataItemCount = 0
556
586
  itemsByIdentity = [:]
557
587
  renderItem = nil
558
588
  recycleItem = nil
@@ -638,11 +668,11 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
638
668
  guard !isApplyingSnapshot, pendingApply == nil,
639
669
  let endReached = endReached,
640
670
  let lastVisible = indexPathsForVisibleItems.map(\.item).max() else { return }
641
- let dataIndices = items.indices.filter { !items[$0].isChrome }
642
- guard !dataIndices.isEmpty,
643
- let lastDataOrdinal = dataIndices.lastIndex(where: { $0 <= lastVisible }) else { return }
644
- let remaining = dataIndices.count - lastDataOrdinal - 1
645
- let thresholdItems = max(1, Int(ceil(Double(dataIndices.count) * endReachedThreshold)))
671
+ guard dataItemCount > 0, dataOrdinals.indices.contains(lastVisible) else { return }
672
+ let lastDataOrdinal = dataOrdinals[lastVisible]
673
+ guard lastDataOrdinal >= 0 else { return }
674
+ let remaining = dataItemCount - lastDataOrdinal - 1
675
+ let thresholdItems = max(1, Int(ceil(Double(dataItemCount) * endReachedThreshold)))
646
676
  guard remaining <= thresholdItems else { return }
647
677
  let signature = dataContentSignature
648
678
  guard endReachedSignature != signature else { return }
@@ -6919,10 +6919,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6919
6919
  }
6920
6920
  // The app script has no screen, so no bar and no tab bar to change.
6921
6921
  guard !isAppScript else { return }
6922
+ // Prepared tabs have a window and a top controller too. Keep their
6923
+ // options, but only the focused screen may change the shared chrome.
6922
6924
  if options.keys.contains("tabAccessory") {
6923
6925
  tabAccessory = options["tabAccessory"] as? [String: Any]
6924
6926
  tabAccessoryDeclared = true
6925
- if navigationController?.topViewController === self, view.window != nil {
6927
+ if navigationController?.topViewController === self, isShownOnScreen {
6926
6928
  CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
6927
6929
  }
6928
6930
  }
@@ -6941,7 +6943,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6941
6943
  }
6942
6944
  if let hidden = options["tabBarHidden"] as? Bool {
6943
6945
  tabBarHidden = hidden
6944
- if navigationController?.topViewController === self, view.window != nil {
6946
+ if navigationController?.topViewController === self, isShownOnScreen {
6945
6947
  CraftChrome.shared.setHiddenByScreen(hidden)
6946
6948
  }
6947
6949
  }
package/dist/package.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Enables users to create installers for their Craft applications
5
5
  * across all platforms (macOS, Windows, Linux)
6
6
  */
7
+ import type { WindowOptions } from './types.js';
7
8
  /** Runtime packages required by the default Linux DEB, including notify-send. */
8
9
  export declare const DEFAULT_DEB_DEPENDENCIES: readonly ['libgtk-3-0', 'libwebkit2gtk-4.1-37', 'libnotify-bin'];
9
10
  export interface PackageConfig {
@@ -89,6 +90,10 @@ export interface PackageConfig {
89
90
  * the GUI process to its own CFBundleExecutable.
90
91
  */
91
92
  nativeRuntimePath?: string;
93
+ /** Window settings for Craft's sandboxed App Store UI host. The compiled
94
+ * binary becomes its inherited backend and implements the bundle protocol.
95
+ */
96
+ nativeWindow?: Pick<WindowOptions, 'width' | 'height' | 'x' | 'y' | 'headless' | 'systemTray' | 'keepRunning' | 'hideDockIcon' | 'titlebarHidden' | 'frameless' | 'transparent' | 'alwaysOnTop' | 'resizable' | 'darkMode' | 'trayPopover'>;
92
97
  /**
93
98
  * Extra executables copied into `Contents/MacOS/` beside the app binary.
94
99
  *
@@ -175,6 +180,10 @@ export declare function formatPackagingCommandError(tool: string, code: number |
175
180
  * Package a Craft application for distribution
176
181
  */
177
182
  export declare function packageApp(config: PackageConfig): Promise<PackageResult[]>;
183
+ /** Select the public-only runtime shipped beside Pantry's direct runtime.
184
+ * App Store packaging fails closed if an older provider has no public build.
185
+ */
186
+ export declare function macOSAppStoreRuntimePath(providerPath: string): string;
178
187
  export interface MacOSBundleMetadata {
179
188
  name: string;
180
189
  /** Actual executable basename when it differs from the app's display name. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.330",
3
+ "version": "0.0.340",
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>",