craft-native 0.0.206 → 0.0.209

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
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3424
3424
  `);
3425
3425
  }
3426
3426
  // package.json
3427
- var version = "0.0.206";
3427
+ var version = "0.0.209";
3428
3428
 
3429
3429
  // bin/cli.ts
3430
3430
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -309,7 +309,7 @@ function craftBinaryNotFoundMessage(triedPath) {
309
309
  `);
310
310
  }
311
311
  // package.json
312
- var version = "0.0.206";
312
+ var version = "0.0.209";
313
313
 
314
314
  // src/sdk-version.ts
315
315
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ function craftBinaryNotFoundMessage(triedPath) {
57
57
  `);
58
58
  }
59
59
  // package.json
60
- var version = "0.0.206";
60
+ var version = "0.0.209";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -621,6 +621,7 @@ final class CraftChrome: ObservableObject {
621
621
  }
622
622
 
623
623
  @Published var tabs: [Tab] = []
624
+ @Published var tabAccessory: Tab?
624
625
  @Published var selected: String?
625
626
  @Published var tabBarVisible = false
626
627
  @Published var splashVisible = true
@@ -688,6 +689,16 @@ final class CraftChrome: ObservableObject {
688
689
  tabs[index].badge = (item["badge"] as? String).flatMap { $0.isEmpty ? nil : String($0.prefix(8)) }
689
690
  }
690
691
  }
692
+ /// A destination beside the tab pill, outside the tab stacks and swipe order.
693
+ func updateTabAccessory(_ item: [String: Any]?) {
694
+ guard let item, let path = item["path"] as? String,
695
+ path.hasPrefix("/"), !path.hasPrefix("//"),
696
+ let title = item["title"] as? String else { tabAccessory = nil; return }
697
+ tabAccessory = Tab(id: path, title: title, symbol: item["symbol"] as? String ?? "message",
698
+ badge: (item["badge"] as? String).flatMap { $0.isEmpty ? nil : String($0.prefix(8)) },
699
+ accessibilityLabel: item["accessibilityLabel"] as? String)
700
+ }
701
+
691
702
  func setHiddenByScreen(_ hidden: Bool) {
692
703
  guard hiddenByScreen != hidden else { return }
693
704
  hiddenByScreen = hidden
@@ -913,7 +924,31 @@ struct CraftTabBarView: View {
913
924
 
914
925
  var body: some View {
915
926
  if chrome.tabBarVisible && !chrome.tabs.isEmpty {
916
- bar
927
+ HStack(spacing: 8) {
928
+ bar
929
+ if let accessory = chrome.tabAccessory {
930
+ Button { CraftHybrid.shared.open(accessory.id) } label: {
931
+ Image(systemName: accessory.symbol)
932
+ .font(.system(size: iconSize))
933
+ .frame(width: 62, height: 62)
934
+ .background(.ultraThinMaterial, in: Circle())
935
+ .overlay(alignment: .topTrailing) {
936
+ if let badge = accessory.badge {
937
+ Text(badge)
938
+ .font(.system(size: badgeSize, weight: .bold))
939
+ .foregroundStyle(.white)
940
+ .padding(.horizontal, 4)
941
+ .frame(minWidth: 18, minHeight: 18)
942
+ .background(Capsule().fill(Color.red))
943
+ }
944
+ }
945
+ }
946
+ .buttonStyle(.plain)
947
+ .accessibilityLabel(accessory.accessibilityLabel ?? accessory.title)
948
+ .accessibilityValue(accessory.badge ?? "")
949
+ .accessibilityIdentifier("craft-tab-accessory")
950
+ }
951
+ }
917
952
  .padding(.horizontal, 16)
918
953
  .padding(.bottom, 4)
919
954
  .background(GeometryReader { proxy in
@@ -854,9 +854,11 @@ final class CraftHybrid: NSObject {
854
854
  let show: (CraftHybridController) -> Void = { [weak self] controller in
855
855
  if native { controller.showNative(path: path, replace: false, fromLink: true) } else { self?.openOutside(url.absoluteString) }
856
856
  }
857
- guard let controller else {
857
+ guard let controller, controller.isViewLoaded else {
858
858
  // Before the shell: a hybrid app's page has it; an all-native app
859
859
  // opens it once the shell is up (a link that launched the app).
860
+ // Construction assigns controller before viewDidLoad makes the
861
+ // navigation stacks; a link in that interval must wait too.
860
862
  guard !hasWeb else { return false }
861
863
  untilShell.append(show)
862
864
  return true
@@ -21,6 +21,13 @@ private func number(_ value: Any?) -> CGFloat? {
21
21
  return value.isFinite ? value : nil
22
22
  }
23
23
 
24
+ private func craftNativeFontWeight(_ value: Any?) -> String? {
25
+ if let value = value as? String { return value }
26
+ guard !(value is Bool), let number = value as? NSNumber, number.doubleValue.isFinite else { return nil }
27
+ let integer = number.doubleValue.rounded()
28
+ return integer == number.doubleValue ? String(Int(integer)) : nil
29
+ }
30
+
24
31
  private func craftNativePickerValue(_ value: Any?) -> String? {
25
32
  if let value = value as? String, !value.isEmpty { return value }
26
33
  if let value = value as? NSNumber, value.doubleValue.isFinite { return String(value.doubleValue) }
@@ -1228,6 +1235,11 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
1228
1235
  return MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), span: MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta))
1229
1236
  }
1230
1237
 
1238
+ private func nonEmptyHandler(_ handler: String?) -> String? {
1239
+ guard let handler, !handler.isEmpty else { return nil }
1240
+ return handler
1241
+ }
1242
+
1231
1243
  private static func color(_ value: String?) -> UIColor {
1232
1244
  switch value?.lowercased() {
1233
1245
  case "red": return .systemRed
@@ -3947,11 +3959,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3947
3959
  // Each screen says whether it wants the tab bar, so one that hid it
3948
3960
  // never leaves it hidden for the next.
3949
3961
  CraftChrome.shared.setHiddenByScreen(tabBarHidden)
3962
+ CraftChrome.shared.updateTabAccessory(tabAccessory)
3950
3963
  refreshTraitDefaults()
3951
3964
  }
3952
3965
 
3953
3966
  override func viewWillDisappear(_ animated: Bool) {
3954
3967
  super.viewWillDisappear(animated)
3968
+ // The accessory belongs to the visible native screen. Clear the
3969
+ // shared shell state before a hybrid/web route appears, otherwise a
3970
+ // destination from the outgoing screen remains tappable on top of it.
3971
+ CraftChrome.shared.updateTabAccessory(nil)
3955
3972
  // Popped (or its stack dismissed) while the bar was away: it comes
3956
3973
  // back for whatever shows next, a web page included.
3957
3974
  if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
@@ -4517,6 +4534,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4517
4534
  private var statusBarHiddenOverride: Bool?
4518
4535
  /// The app's tab bar away while this screen shows (`tabBarHidden`).
4519
4536
  private var tabBarHidden = false
4537
+ private var tabAccessory: [String: Any]?
4520
4538
  /// Whether a back-swipe may leave this screen at all, the edge's
4521
4539
  /// included (`swipeBack: false`): a screen that steps through its own
4522
4540
  /// pages with a Back of its own (a first-run tour's questions) would
@@ -4563,6 +4581,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4563
4581
  }
4564
4582
  // The app script has no screen, so no bar and no tab bar to change.
4565
4583
  guard !isAppScript else { return }
4584
+ if options.keys.contains("tabAccessory") {
4585
+ tabAccessory = options["tabAccessory"] as? [String: Any]
4586
+ if navigationController?.topViewController === self, view.window != nil {
4587
+ CraftChrome.shared.updateTabAccessory(tabAccessory)
4588
+ }
4589
+ }
4566
4590
  if options.keys.contains("title") {
4567
4591
  navigationItem.title = options["title"] as? String
4568
4592
  titleFromOptions = true
@@ -6610,7 +6634,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6610
6634
  (icon as? CraftNativeIconView)?.defaultBox = box == nil && fontSize == nil ? 20 : nil
6611
6635
  let pointSize = fontSize ?? (box ?? 20) * 0.84
6612
6636
  let weight: UIImage.SymbolWeight
6613
- switch style["fontWeight"] as? String {
6637
+ switch craftNativeFontWeight(style["fontWeight"]) {
6614
6638
  case "100": weight = .ultraLight
6615
6639
  case "200": weight = .thin
6616
6640
  case "300": weight = .light
@@ -6927,8 +6951,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6927
6951
  case "900": weight = .black
6928
6952
  default: weight = .regular
6929
6953
  }
6930
- var font = (style["fontFamily"] as? String).flatMap { UIFont(name: $0, size: size) }
6931
- ?? .systemFont(ofSize: size, weight: weight)
6954
+ // Android's TextView uses SP, so its native text scales with the
6955
+ // user's content-size setting. Apply the same Dynamic Type multiplier
6956
+ // here before assigning the font to labels, buttons, and inputs.
6957
+ let scaledSize = UIFontMetrics(forTextStyle: .body).scaledValue(
6958
+ for: size,
6959
+ compatibleWith: traitCollection
6960
+ )
6961
+ var font = (style["fontFamily"] as? String).flatMap { UIFont(name: $0, size: scaledSize) }
6962
+ ?? .systemFont(ofSize: scaledSize, weight: weight)
6932
6963
  if style["fontStyle"] as? String == "italic",
6933
6964
  let descriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
6934
6965
  font = UIFont(descriptor: descriptor, size: size)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.206",
3
+ "version": "0.0.209",
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>",