@craft-native/ios 0.0.207 → 0.0.210

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/README.md CHANGED
@@ -137,7 +137,13 @@ and linear gradients are applied by UIKit with the same style keys as Android;
137
137
  `elevation` remains Android-specific. Gradient layers follow the native view's
138
138
  bounds and corners through layout changes.
139
139
 
140
- The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `ImageBackground`, `Video`, `Map`, `Svg`, `G`, `SvgText`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `Map` uses MapKit for trail polylines and marker taps; its overlays carry flat latitude/longitude `geometry` arrays, `strokeWidth` is expressed in logical points, and the camera supports controlled `region`, one-time `initialRegion`, and `fitToBounds`. `Svg` is a DOM-free drawing surface; grouped `G` and `SvgText` nodes are painted inside it. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond timestamps; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
140
+ The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `KeyboardAvoidingView`, `StatusBar`, `RefreshControl`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `ImageBackground`, `Video`, `Map`, `Svg`, `G`, `SvgText`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `StatusBar` and `RefreshControl` are screen/scroll utility nodes rather than visible children. `Map` uses MapKit for trail polylines and marker taps; its overlays carry flat latitude/longitude `geometry` arrays, `strokeWidth` is expressed in logical points, and the camera supports controlled `region`, one-time `initialRegion`, and `fitToBounds`. `Svg` is a DOM-free drawing surface; grouped `G` and `SvgText` nodes are painted inside it. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond `timestamp` fields; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. `craft.navigation.setOptions` also accepts a `tabAccessory` destination beside the tab pill; it uses an app-local `path`, `title`, `symbol`, optional `badge`, and `accessibilityLabel`, and `null` clears it. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
141
+
142
+ JavaScriptCore has no DOM `window`; Craft aliases `globalThis.window` to
143
+ `globalThis` before installing the native bridge so browser-safe SDK calls such
144
+ as `window.craft.dialog.actionSheet()` reach the native capability host. This
145
+ does not add `document` or DOM rendering; routes that do not opt into `native`
146
+ continue using the WebView renderer.
141
147
 
142
148
  ```bash
143
149
  craft ios init NativeDemo --renderer native --output ./ios-native
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.207",
3
+ "version": "0.0.210",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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,33 @@ 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
+ surface {
932
+ Image(systemName: accessory.symbol)
933
+ .font(.system(size: iconSize))
934
+ .foregroundStyle(Color.primary)
935
+ .frame(width: 62, height: 62)
936
+ }
937
+ .overlay(alignment: .topTrailing) {
938
+ if let badge = accessory.badge {
939
+ Text(badge)
940
+ .font(.system(size: badgeSize, weight: .bold))
941
+ .foregroundStyle(.white)
942
+ .padding(.horizontal, 4)
943
+ .frame(minWidth: 18, minHeight: 18)
944
+ .background(Capsule().fill(Color.red))
945
+ }
946
+ }
947
+ }
948
+ .buttonStyle(.plain)
949
+ .accessibilityLabel(accessory.accessibilityLabel ?? accessory.title)
950
+ .accessibilityValue(accessory.badge ?? "")
951
+ .accessibilityIdentifier("craft-tab-accessory")
952
+ }
953
+ }
917
954
  .padding(.horizontal, 16)
918
955
  .padding(.bottom, 4)
919
956
  .background(GeometryReader { proxy in
@@ -932,24 +969,27 @@ struct CraftTabBarView: View {
932
969
  chrome.layoutChanged(max(0, screen - proxy.frame(in: .global).minY))
933
970
  }
934
971
 
935
- @ViewBuilder private var bar: some View {
972
+ private var bar: some View {
973
+ surface { items.padding(4) }
974
+ }
975
+
976
+ /// Both the tab capsule and its circular accessory use the same material,
977
+ /// outline and shadow. A square capsule forms the accessory's circle.
978
+ @ViewBuilder private func surface<Content: View>(@ViewBuilder content: () -> Content) -> some View {
936
979
  #if compiler(>=6.2)
937
980
  if #available(iOS 26.0, *) {
938
981
  GlassEffectContainer {
939
- items
940
- .padding(4)
982
+ content()
941
983
  .glassEffect(.regular.interactive(), in: .capsule)
942
984
  }
943
985
  } else {
944
- items
945
- .padding(4)
986
+ content()
946
987
  .background(.ultraThinMaterial, in: Capsule())
947
988
  .overlay(Capsule().strokeBorder(Color.white.opacity(0.35), lineWidth: 0.5))
948
989
  .shadow(color: .black.opacity(0.12), radius: 18, y: 6)
949
990
  }
950
991
  #else
951
- items
952
- .padding(4)
992
+ content()
953
993
  .background(.ultraThinMaterial, in: Capsule())
954
994
  .overlay(Capsule().strokeBorder(Color.white.opacity(0.35), lineWidth: 0.5))
955
995
  .shadow(color: .black.opacity(0.12), radius: 18, y: 6)
@@ -3959,11 +3959,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3959
3959
  // Each screen says whether it wants the tab bar, so one that hid it
3960
3960
  // never leaves it hidden for the next.
3961
3961
  CraftChrome.shared.setHiddenByScreen(tabBarHidden)
3962
+ CraftChrome.shared.updateTabAccessory(tabAccessory)
3962
3963
  refreshTraitDefaults()
3963
3964
  }
3964
3965
 
3965
3966
  override func viewWillDisappear(_ animated: Bool) {
3966
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)
3967
3972
  // Popped (or its stack dismissed) while the bar was away: it comes
3968
3973
  // back for whatever shows next, a web page included.
3969
3974
  if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
@@ -4146,6 +4151,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4146
4151
  let appVersion = Self.jsonString(info?["CFBundleShortVersionString"] as? String ?? "")
4147
4152
  let appBuild = Self.jsonString(info?["CFBundleVersion"] as? String ?? "")
4148
4153
  jsContext.evaluateScript("""
4154
+ // JavaScriptCore has no DOM window, but browser-safe SDK bridges
4155
+ // address the host through `window.craft`. Keep native screens
4156
+ // compatible without pretending a document exists.
4157
+ if (typeof globalThis.window === "undefined") { globalThis.window = globalThis; }
4149
4158
  globalThis.__stxNativeCallback = null;
4150
4159
  globalThis.__stxNativeBridge = {
4151
4160
  platform: "ios",
@@ -4274,7 +4283,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4274
4283
  /// `craft.secureStorage.set` wrote.
4275
4284
  /// - `craft.navigation.setOptions({ title, largeTitle, hidden, backTitle,
4276
4285
  /// tabBarHidden, swipeBack, contentSwipeBack, tabSwipeEnabled,
4277
- /// backgroundColor, navigationBarSolid, rightButtons })`;
4286
+ /// backgroundColor, navigationBarSolid, rightButtons, tabAccessory })`;
4278
4287
  /// a right button's tap is the event `navButton`.
4279
4288
  ///
4280
4289
  /// A runtime that assigns `craft.storage` (or the others) afterwards keeps
@@ -4529,6 +4538,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4529
4538
  private var statusBarHiddenOverride: Bool?
4530
4539
  /// The app's tab bar away while this screen shows (`tabBarHidden`).
4531
4540
  private var tabBarHidden = false
4541
+ private var tabAccessory: [String: Any]?
4532
4542
  /// Whether a back-swipe may leave this screen at all, the edge's
4533
4543
  /// included (`swipeBack: false`): a screen that steps through its own
4534
4544
  /// pages with a Back of its own (a first-run tour's questions) would
@@ -4575,6 +4585,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4575
4585
  }
4576
4586
  // The app script has no screen, so no bar and no tab bar to change.
4577
4587
  guard !isAppScript else { return }
4588
+ if options.keys.contains("tabAccessory") {
4589
+ tabAccessory = options["tabAccessory"] as? [String: Any]
4590
+ if navigationController?.topViewController === self, view.window != nil {
4591
+ CraftChrome.shared.updateTabAccessory(tabAccessory)
4592
+ }
4593
+ }
4578
4594
  if options.keys.contains("title") {
4579
4595
  navigationItem.title = options["title"] as? String
4580
4596
  titleFromOptions = true
@@ -5719,7 +5735,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5719
5735
  ? nil : number(props["activeOpacity"]) ?? 0.5
5720
5736
  pressable.pressBaseOpacity = number(style["opacity"]) ?? 1
5721
5737
  pressable.pressUnderlayColor = type == "TouchableHighlight" && !disabled ? color(props["underlayColor"]) : nil
5722
- pressable.pressBaseBackgroundColor = pressable.backgroundColor
5723
5738
  case "Switch":
5724
5739
  let toggle = result as! UISwitch
5725
5740
  toggle.isOn = props["value"] as? Bool ?? props["checked"] as? Bool ?? false
@@ -6677,6 +6692,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6677
6692
  updateDimension(width, constraint: &node.widthConstraint, anchor: view.widthAnchor)
6678
6693
  updateDimension(height, constraint: &node.heightConstraint, anchor: view.heightAnchor)
6679
6694
  view.backgroundColor = color(style["backgroundColor"]) ?? .clear
6695
+ // Every flow container can receive press feedback, including ordinary
6696
+ // Views. Capture its current authored fill before feedback cleanup or
6697
+ // release restores it; otherwise avatars and button wrappers go clear.
6698
+ (view as? CraftNativeFlowView)?.pressBaseBackgroundColor = view.backgroundColor
6680
6699
  // A field the style dresses itself (`border-0`, a border or a
6681
6700
  // background, as a search field inside its own pill is) draws no
6682
6701
  // system box of its own; a bare one keeps the rounded rectangle.
@@ -6928,7 +6947,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6928
6947
  private func textFont(_ style: [String: Any], default fallback: UIFont) -> UIFont {
6929
6948
  let size = number(style["fontSize"]) ?? fallback.pointSize
6930
6949
  let weight: UIFont.Weight
6931
- switch style["fontWeight"] as? String {
6950
+ switch craftNativeFontWeight(style["fontWeight"]) {
6932
6951
  case "100": weight = .ultraLight
6933
6952
  case "200": weight = .thin
6934
6953
  case "300": weight = .light
@@ -6939,11 +6958,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6939
6958
  case "900": weight = .black
6940
6959
  default: weight = .regular
6941
6960
  }
6942
- var font = (style["fontFamily"] as? String).flatMap { UIFont(name: $0, size: size) }
6943
- ?? .systemFont(ofSize: size, weight: weight)
6961
+ // Android's TextView uses SP, so its native text scales with the
6962
+ // user's content-size setting. Apply the same Dynamic Type multiplier
6963
+ // here before assigning the font to labels, buttons, and inputs.
6964
+ let scaledSize = UIFontMetrics(forTextStyle: .body).scaledValue(
6965
+ for: size,
6966
+ compatibleWith: traitCollection
6967
+ )
6968
+ var font = (style["fontFamily"] as? String).flatMap { UIFont(name: $0, size: scaledSize) }
6969
+ ?? .systemFont(ofSize: scaledSize, weight: weight)
6944
6970
  if style["fontStyle"] as? String == "italic",
6945
6971
  let descriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
6946
- font = UIFont(descriptor: descriptor, size: size)
6972
+ font = UIFont(descriptor: descriptor, size: scaledSize)
6947
6973
  }
6948
6974
  return font
6949
6975
  }
@@ -7143,7 +7169,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
7143
7169
  || controlledVideo
7144
7170
  view.isAccessibilityElement = !container && role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "DatePicker", "Switch", "Slider"].contains(type))
7145
7171
  view.accessibilityContainerType = container && (view.accessibilityLabel != nil || controlledVideo) ? .semanticGroup : .none
7172
+ // Keep the interaction trait native controls expose. Rebuilding this
7173
+ // set from the STX role alone otherwise turns a Slider, Picker,
7174
+ // DatePicker, or Switch into a static VoiceOver item whenever the
7175
+ // screen omits redundant accessibility metadata.
7146
7176
  var traits: UIAccessibilityTraits = []
7177
+ switch type {
7178
+ case "Slider", "Picker", "DatePicker": traits.insert(.adjustable)
7179
+ case "Switch": traits.insert(.button)
7180
+ default: break
7181
+ }
7147
7182
  switch role ?? type.lowercased() {
7148
7183
  case "tablist": traits.insert(.tabBar)
7149
7184
  case "button", "checkbox": traits.insert(.button)