craft-native 0.0.209 → 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.
@@ -389,6 +389,10 @@ internal object CraftNativePrelude {
389
389
  routePrelude: String,
390
390
  ): String = """
391
391
  $routePrelude
392
+ // JavaScriptSandbox has no DOM window, but browser-safe SDK bridges
393
+ // address the host through `window.craft`. Keep native screens
394
+ // compatible without pretending a document exists.
395
+ if (typeof globalThis.window === 'undefined') globalThis.window = globalThis;
392
396
  (function() {
393
397
  var forward = null;
394
398
  var queued = [];
@@ -567,7 +571,7 @@ internal object CraftNativePrelude {
567
571
  * The bar above a screen (back, title, the screen's buttons) and the tab bar
568
572
  * below it, in plain Android views. A screen sets its bar with
569
573
  * `craft.navigation.setOptions` (title, largeTitle, hidden, rightButtons,
570
- * tabBarHidden); the tabs come from the app's `tabs`.
574
+ * tabAccessory, tabBarHidden); the tabs come from the app's `tabs`.
571
575
  */
572
576
  internal class CraftNativeChrome(context: Context, private val tabs: List<CraftNativeTab>) : LinearLayout(context) {
573
577
  val content = FrameLayout(context)
@@ -718,12 +718,15 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
718
718
  val explicitRegion = region(pendingProps.optJSONObject("region"))
719
719
  val initialRegion = region(pendingProps.optJSONObject("initialRegion"))
720
720
  if (explicitRegion != null) {
721
- moveCamera(explicitRegion)
721
+ // A controlled region is an exact camera request. The padding
722
+ // used for fitToBounds is framing chrome, not part of the region
723
+ // contract, and would otherwise zoom every Android screen out.
724
+ moveCamera(explicitRegion, padding = 0)
722
725
  initialCameraKey = null
723
726
  } else if (initialRegion != null) {
724
727
  val key = "region:${pendingProps.optJSONObject("initialRegion")}"
725
728
  if (initialCameraKey != key) {
726
- moveCamera(initialRegion)
729
+ moveCamera(initialRegion, padding = 0)
727
730
  initialCameraKey = key
728
731
  }
729
732
  } else if (craftNativeBoolean(pendingProps.opt("fitToBounds"))) {
@@ -745,9 +748,9 @@ private class CraftNativeMapView(context: Context) : FrameLayout(context), OnMap
745
748
  }
746
749
  }
747
750
 
748
- private fun moveCamera(bounds: LatLngBounds) {
751
+ private fun moveCamera(bounds: LatLngBounds, padding: Int = 24) {
749
752
  post {
750
- if (width > 0 && height > 0) googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 24))
753
+ if (width > 0 && height > 0) googleMap?.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, padding))
751
754
  }
752
755
  }
753
756
 
@@ -6275,7 +6278,7 @@ class MainActivity : AppCompatActivity() {
6275
6278
  private fun configureTextStyle(view: TextView, style: JSONObject) {
6276
6279
  view.setTextColor(color(craftNativeOptionalString(style.opt("color")).orEmpty(), defaultTextColor))
6277
6280
  view.setTextSize(TypedValue.COMPLEX_UNIT_SP, craftNativeFiniteFloat(style, "fontSize", 16f))
6278
- val fontWeight = style.opt("fontWeight") as? String
6281
+ val fontWeight = craftNativeFontWeight(style.opt("fontWeight"))
6279
6282
  val weight = when (fontWeight) {
6280
6283
  "bold" -> 700
6281
6284
  "100", "200", "300", "400", "500", "600", "700", "800", "900" -> style.optInt("fontWeight")
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.209";
3427
+ var version = "0.0.210";
3428
3428
 
3429
3429
  // bin/cli.ts
3430
3430
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -245,7 +245,7 @@ export interface TextStyleProps {
245
245
  /** Font style */
246
246
  fontStyle?: 'normal' | 'italic';
247
247
  /** Font weight */
248
- fontWeight?: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
248
+ fontWeight?: number | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
249
249
  /** Letter spacing */
250
250
  letterSpacing?: number;
251
251
  /** Line height */
@@ -287,6 +287,8 @@ export interface ImageStyleProps {
287
287
  * Combined Image style type.
288
288
  */
289
289
  export type ImageStyle = ViewStyle & ImageStyleProps;
290
+ /** A native style may be nested or conditionally omitted in any component's `style` prop. */
291
+ export type StyleValue<T> = T | StyleValue<T>[] | null | false | undefined;
290
292
  /**
291
293
  * Base props for all components.
292
294
  */
@@ -320,7 +322,7 @@ export interface AccessibilityState {
320
322
  */
321
323
  export interface ViewProps extends BaseProps {
322
324
  /** Style object or array */
323
- style?: ViewStyle | ViewStyle[];
325
+ style?: StyleValue<ViewStyle>;
324
326
  /** Press handler */
325
327
  onPress?: () => void;
326
328
  /** DOM-compatible press alias accepted by native hosts. */
@@ -356,19 +358,23 @@ export interface RefreshControlProps extends BaseProps {
356
358
  */
357
359
  export interface TextProps extends BaseProps {
358
360
  /** Style object or array */
359
- style?: TextStyle | TextStyle[];
361
+ style?: StyleValue<TextStyle>;
360
362
  /** Number of lines before truncation */
361
363
  numberOfLines?: number;
362
364
  /** Ellipsize mode */
363
365
  ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
364
366
  /** Press handler */
365
367
  onPress?: () => void;
368
+ /** DOM-compatible press alias accepted by native hosts. */
369
+ onClick?: () => void;
366
370
  /** Long press handler */
367
371
  onLongPress?: () => void;
368
372
  }
369
373
  /** Native button/link action props. */
370
374
  export interface ButtonProps extends BaseProps {
371
375
  title?: string;
376
+ /** Text and layout styling applied by the native button hosts. */
377
+ style?: StyleValue<TextStyle>;
372
378
  onPress?: () => void;
373
379
  onClick?: () => void;
374
380
  disabled?: boolean;
@@ -393,10 +399,17 @@ export interface TouchableHighlightProps extends ViewProps {
393
399
  /**
394
400
  * Image source type.
395
401
  */
396
- export type ImageSource = {
402
+ export type ImageSource = string | {
397
403
  uri: string;
398
404
  headers?: Record<string, string>;
399
405
  };
406
+ export interface ImageErrorEvent {
407
+ nativeEvent: {
408
+ error: {
409
+ message: string;
410
+ };
411
+ };
412
+ }
400
413
  /**
401
414
  * Image component props.
402
415
  */
@@ -404,7 +417,7 @@ export interface ImageProps extends BaseProps {
404
417
  /** Image source */
405
418
  source: ImageSource;
406
419
  /** Style object or array */
407
- style?: ImageStyle | ImageStyle[];
420
+ style?: StyleValue<ImageStyle>;
408
421
  /** Resize mode */
409
422
  resizeMode?: 'cover' | 'contain' | 'stretch' | 'center';
410
423
  /** Load start handler */
@@ -413,19 +426,21 @@ export interface ImageProps extends BaseProps {
413
426
  onLoad?: () => void;
414
427
  /** Load end handler */
415
428
  onLoadEnd?: () => void;
416
- /** Error handler */
417
- onError?: (error: Error) => void;
429
+ /** Error handler; native failures are exposed as nativeEvent.error.message. */
430
+ onError?: (event: ImageErrorEvent) => void;
418
431
  /** Default source while loading */
419
432
  defaultSource?: ImageSource;
420
433
  /** Alt text for accessibility */
421
434
  alt?: string;
422
435
  }
423
436
  export interface IconProps extends BaseProps {
437
+ style?: StyleValue<TextStyle>;
424
438
  name?: string;
425
439
  symbol?: string;
426
440
  color?: string;
427
441
  }
428
442
  export interface VideoProps extends BaseProps {
443
+ style?: StyleValue<ViewStyle>;
429
444
  source?: string | {
430
445
  uri: string;
431
446
  };
@@ -472,6 +487,7 @@ export interface MapRegion {
472
487
  };
473
488
  }
474
489
  export interface MapProps extends BaseProps {
490
+ style?: StyleValue<ViewStyle>;
475
491
  region?: MapRegion;
476
492
  /** Initial camera region; applied once, then controlled by subsequent props. */
477
493
  initialRegion?: MapRegion;
@@ -512,7 +528,7 @@ export interface ImageBackgroundProps extends ImageProps {
512
528
  }
513
529
  /** Shared paint and accessibility props for DOM-free native SVG nodes. */
514
530
  export interface SvgPaintProps extends BaseProps {
515
- style?: ViewStyle | ViewStyle[];
531
+ style?: StyleValue<ViewStyle>;
516
532
  fill?: string;
517
533
  stroke?: string;
518
534
  strokeWidth?: number;
@@ -629,7 +645,7 @@ export interface PressableProps extends ViewProps {
629
645
  */
630
646
  export interface TextInputProps extends BaseProps {
631
647
  /** Style object or array */
632
- style?: TextStyle | TextStyle[];
648
+ style?: StyleValue<TextStyle>;
633
649
  /** Current value */
634
650
  value?: string;
635
651
  /** Default value */
@@ -680,6 +696,7 @@ export interface TextInputProps extends BaseProps {
680
696
  export type KeyboardType = 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'number-pad' | 'decimal-pad' | 'url' | 'web-search' | 'visible-password';
681
697
  export type ReturnKeyType = 'done' | 'go' | 'next' | 'search' | 'send' | 'join' | 'route' | 'continue' | 'emergency-call' | 'default';
682
698
  export interface SwitchProps extends BaseProps {
699
+ style?: StyleValue<ViewStyle>;
683
700
  /** Controlled boolean state; malformed native bridge values stay off. */
684
701
  value?: boolean;
685
702
  onValueChange?: (value: boolean) => void;
@@ -693,6 +710,7 @@ export interface SwitchProps extends BaseProps {
693
710
  ios_backgroundColor?: string;
694
711
  }
695
712
  export interface CheckboxProps extends BaseProps {
713
+ style?: StyleValue<ViewStyle>;
696
714
  /** Controlled boolean state; malformed native bridge values stay off. */
697
715
  value?: boolean;
698
716
  checked?: boolean;
@@ -704,6 +722,7 @@ export interface CheckboxProps extends BaseProps {
704
722
  color?: string;
705
723
  }
706
724
  export interface SliderProps extends BaseProps {
725
+ style?: StyleValue<ViewStyle>;
707
726
  value?: number;
708
727
  minimumValue?: number;
709
728
  maximumValue?: number;
@@ -722,6 +741,7 @@ export interface PickerOption {
722
741
  value: string | number;
723
742
  }
724
743
  export interface PickerProps extends BaseProps {
744
+ style?: StyleValue<ViewStyle>;
725
745
  /** Typed options; child options remain supported when omitted. */
726
746
  items?: PickerOption[];
727
747
  selectedValue?: string | number;
@@ -735,7 +755,8 @@ export interface DatePickerChange {
735
755
  timestamp: number;
736
756
  }
737
757
  export interface DatePickerProps extends BaseProps {
738
- mode?: 'date' | 'time' | 'datetime' | 'countdown';
758
+ style?: StyleValue<ViewStyle>;
759
+ mode?: 'date' | 'time' | 'datetime' | 'datetime-local' | 'countdown';
739
760
  value?: string | number;
740
761
  minimumDate?: string | number;
741
762
  maximumDate?: string | number;
@@ -745,6 +766,7 @@ export interface DatePickerProps extends BaseProps {
745
766
  disabled?: boolean;
746
767
  }
747
768
  export interface ActivityIndicatorProps extends BaseProps {
769
+ style?: StyleValue<ViewStyle>;
748
770
  color?: string;
749
771
  animating?: boolean;
750
772
  hidesWhenStopped?: boolean;
@@ -782,6 +804,7 @@ export interface SectionListSection<T> {
782
804
  export interface SectionListProps<T> extends ScrollViewProps {
783
805
  }
784
806
  export interface ModalProps extends BaseProps {
807
+ style?: StyleValue<ViewStyle>;
785
808
  visible?: boolean;
786
809
  transparent?: boolean;
787
810
  animationType?: 'none' | 'fade' | 'slide';
@@ -893,9 +916,9 @@ export declare const StyleSheet: {
893
916
  */
894
917
  create<T extends Record<string, ViewStyle | TextStyle | ImageStyle>>(styles: T): T;
895
918
  /**
896
- * Flatten an array of styles into a single object.
919
+ * Flatten nested style arrays and ignore falsy conditional entries.
897
920
  */
898
- flatten<T>(style: T | T[] | undefined): T | undefined;
921
+ flatten<T>(style: StyleValue<T>): T | undefined;
899
922
  /**
900
923
  * Get the absolute fill style (full parent).
901
924
  */
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.209";
312
+ var version = "0.0.210";
313
313
 
314
314
  // src/sdk-version.ts
315
315
  var SDK_VERSION = version;
@@ -8625,10 +8625,19 @@ var StyleSheet = {
8625
8625
  flatten(style) {
8626
8626
  if (!style)
8627
8627
  return;
8628
- if (Array.isArray(style)) {
8629
- return Object.assign({}, ...style);
8630
- }
8631
- return style;
8628
+ const flattened = {};
8629
+ const merge = (value) => {
8630
+ if (!value)
8631
+ return;
8632
+ if (Array.isArray(value)) {
8633
+ value.forEach(merge);
8634
+ return;
8635
+ }
8636
+ if (typeof value === "object")
8637
+ Object.assign(flattened, value);
8638
+ };
8639
+ merge(style);
8640
+ return flattened;
8632
8641
  },
8633
8642
  absoluteFill: {
8634
8643
  position: "absolute",
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.209";
60
+ var version = "0.0.210";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -8384,10 +8384,19 @@ var StyleSheet = {
8384
8384
  flatten(style) {
8385
8385
  if (!style)
8386
8386
  return;
8387
- if (Array.isArray(style)) {
8388
- return Object.assign({}, ...style);
8389
- }
8390
- return style;
8387
+ const flattened = {};
8388
+ const merge = (value) => {
8389
+ if (!value)
8390
+ return;
8391
+ if (Array.isArray(value)) {
8392
+ value.forEach(merge);
8393
+ return;
8394
+ }
8395
+ if (typeof value === "object")
8396
+ Object.assign(flattened, value);
8397
+ };
8398
+ merge(style);
8399
+ return flattened;
8391
8400
  },
8392
8401
  absoluteFill: {
8393
8402
  position: "absolute",
@@ -928,10 +928,12 @@ struct CraftTabBarView: View {
928
928
  bar
929
929
  if let accessory = chrome.tabAccessory {
930
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())
931
+ surface {
932
+ Image(systemName: accessory.symbol)
933
+ .font(.system(size: iconSize))
934
+ .foregroundStyle(Color.primary)
935
+ .frame(width: 62, height: 62)
936
+ }
935
937
  .overlay(alignment: .topTrailing) {
936
938
  if let badge = accessory.badge {
937
939
  Text(badge)
@@ -967,24 +969,27 @@ struct CraftTabBarView: View {
967
969
  chrome.layoutChanged(max(0, screen - proxy.frame(in: .global).minY))
968
970
  }
969
971
 
970
- @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 {
971
979
  #if compiler(>=6.2)
972
980
  if #available(iOS 26.0, *) {
973
981
  GlassEffectContainer {
974
- items
975
- .padding(4)
982
+ content()
976
983
  .glassEffect(.regular.interactive(), in: .capsule)
977
984
  }
978
985
  } else {
979
- items
980
- .padding(4)
986
+ content()
981
987
  .background(.ultraThinMaterial, in: Capsule())
982
988
  .overlay(Capsule().strokeBorder(Color.white.opacity(0.35), lineWidth: 0.5))
983
989
  .shadow(color: .black.opacity(0.12), radius: 18, y: 6)
984
990
  }
985
991
  #else
986
- items
987
- .padding(4)
992
+ content()
988
993
  .background(.ultraThinMaterial, in: Capsule())
989
994
  .overlay(Capsule().strokeBorder(Color.white.opacity(0.35), lineWidth: 0.5))
990
995
  .shadow(color: .black.opacity(0.12), radius: 18, y: 6)
@@ -4151,6 +4151,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4151
4151
  let appVersion = Self.jsonString(info?["CFBundleShortVersionString"] as? String ?? "")
4152
4152
  let appBuild = Self.jsonString(info?["CFBundleVersion"] as? String ?? "")
4153
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; }
4154
4158
  globalThis.__stxNativeCallback = null;
4155
4159
  globalThis.__stxNativeBridge = {
4156
4160
  platform: "ios",
@@ -4279,7 +4283,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4279
4283
  /// `craft.secureStorage.set` wrote.
4280
4284
  /// - `craft.navigation.setOptions({ title, largeTitle, hidden, backTitle,
4281
4285
  /// tabBarHidden, swipeBack, contentSwipeBack, tabSwipeEnabled,
4282
- /// backgroundColor, navigationBarSolid, rightButtons })`;
4286
+ /// backgroundColor, navigationBarSolid, rightButtons, tabAccessory })`;
4283
4287
  /// a right button's tap is the event `navButton`.
4284
4288
  ///
4285
4289
  /// A runtime that assigns `craft.storage` (or the others) afterwards keeps
@@ -5731,7 +5735,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5731
5735
  ? nil : number(props["activeOpacity"]) ?? 0.5
5732
5736
  pressable.pressBaseOpacity = number(style["opacity"]) ?? 1
5733
5737
  pressable.pressUnderlayColor = type == "TouchableHighlight" && !disabled ? color(props["underlayColor"]) : nil
5734
- pressable.pressBaseBackgroundColor = pressable.backgroundColor
5735
5738
  case "Switch":
5736
5739
  let toggle = result as! UISwitch
5737
5740
  toggle.isOn = props["value"] as? Bool ?? props["checked"] as? Bool ?? false
@@ -6689,6 +6692,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6689
6692
  updateDimension(width, constraint: &node.widthConstraint, anchor: view.widthAnchor)
6690
6693
  updateDimension(height, constraint: &node.heightConstraint, anchor: view.heightAnchor)
6691
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
6692
6699
  // A field the style dresses itself (`border-0`, a border or a
6693
6700
  // background, as a search field inside its own pill is) draws no
6694
6701
  // system box of its own; a bare one keeps the rounded rectangle.
@@ -6940,7 +6947,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6940
6947
  private func textFont(_ style: [String: Any], default fallback: UIFont) -> UIFont {
6941
6948
  let size = number(style["fontSize"]) ?? fallback.pointSize
6942
6949
  let weight: UIFont.Weight
6943
- switch style["fontWeight"] as? String {
6950
+ switch craftNativeFontWeight(style["fontWeight"]) {
6944
6951
  case "100": weight = .ultraLight
6945
6952
  case "200": weight = .thin
6946
6953
  case "300": weight = .light
@@ -6962,7 +6969,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6962
6969
  ?? .systemFont(ofSize: scaledSize, weight: weight)
6963
6970
  if style["fontStyle"] as? String == "italic",
6964
6971
  let descriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
6965
- font = UIFont(descriptor: descriptor, size: size)
6972
+ font = UIFont(descriptor: descriptor, size: scaledSize)
6966
6973
  }
6967
6974
  return font
6968
6975
  }
@@ -7162,7 +7169,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
7162
7169
  || controlledVideo
7163
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))
7164
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.
7165
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
+ }
7166
7182
  switch role ?? type.lowercased() {
7167
7183
  case "tablist": traits.insert(.tabBar)
7168
7184
  case "button", "checkbox": traits.insert(.button)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.209",
3
+ "version": "0.0.210",
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>",