craft-native 0.0.205 → 0.0.207

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.205";
3427
+ var version = "0.0.207";
3428
3428
 
3429
3429
  // bin/cli.ts
3430
3430
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -527,6 +527,8 @@ export interface SvgPaintProps extends BaseProps {
527
527
  vectorEffect?: 'none' | 'non-scaling-stroke';
528
528
  color?: string;
529
529
  opacity?: number;
530
+ /** Font weight inherited by descendant native SVG text. */
531
+ fontWeight?: string | number;
530
532
  }
531
533
  /** Root native SVG drawing surface. */
532
534
  export interface SvgProps extends SvgPaintProps {
@@ -559,7 +561,6 @@ export interface SvgTextProps extends SvgPaintProps {
559
561
  x?: number;
560
562
  y?: number;
561
563
  fontSize?: number;
562
- fontWeight?: string | number;
563
564
  textAnchor?: 'start' | 'middle' | 'end' | string;
564
565
  dominantBaseline?: string;
565
566
  }
@@ -679,6 +680,7 @@ export interface TextInputProps extends BaseProps {
679
680
  export type KeyboardType = 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'number-pad' | 'decimal-pad' | 'url' | 'web-search' | 'visible-password';
680
681
  export type ReturnKeyType = 'done' | 'go' | 'next' | 'search' | 'send' | 'join' | 'route' | 'continue' | 'emergency-call' | 'default';
681
682
  export interface SwitchProps extends BaseProps {
683
+ /** Controlled boolean state; malformed native bridge values stay off. */
682
684
  value?: boolean;
683
685
  onValueChange?: (value: boolean) => void;
684
686
  onChange?: (value: boolean) => void;
@@ -691,6 +693,7 @@ export interface SwitchProps extends BaseProps {
691
693
  ios_backgroundColor?: string;
692
694
  }
693
695
  export interface CheckboxProps extends BaseProps {
696
+ /** Controlled boolean state; malformed native bridge values stay off. */
694
697
  value?: boolean;
695
698
  checked?: boolean;
696
699
  indeterminate?: boolean;
@@ -715,6 +718,7 @@ export interface SliderProps extends BaseProps {
715
718
  }
716
719
  export interface PickerOption {
717
720
  label: string;
721
+ /** String or finite number; other bridge values are ignored by native hosts. */
718
722
  value: string | number;
719
723
  }
720
724
  export interface PickerProps extends BaseProps {
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.205";
312
+ var version = "0.0.207";
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.205";
60
+ var version = "0.0.207";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -102,6 +102,17 @@ function initSimulatorDriver(options) {
102
102
  if (previous !== content)
103
103
  writeFileSync(path, content);
104
104
  }
105
+ const script = join2(output, "UITests/journey.txt");
106
+ if (options.script === undefined)
107
+ rmSync(script, { force: true });
108
+ else {
109
+ let previous;
110
+ try {
111
+ previous = readFileSync(script, "utf8");
112
+ } catch {}
113
+ if (previous !== options.script)
114
+ writeFileSync(script, options.script);
115
+ }
105
116
  }
106
117
  function serializeSimulatorSteps(steps) {
107
118
  const number = (value) => {
@@ -7,6 +7,8 @@ export interface SimulatorDriverOptions {
7
7
  deploymentTarget?: string;
8
8
  controlUrl?: string;
9
9
  tabs?: readonly string[];
10
+ /** Bundle a wire-protocol journey and save native XCTest attachments. No control server is needed on physical devices. */
11
+ script?: string;
10
12
  }
11
13
  /** Generate the reusable XCUITest shell from package templates. No app-owned Swift. */
12
14
  export declare function initSimulatorDriver(options: SimulatorDriverOptions): void;
@@ -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
@@ -348,6 +348,10 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
348
348
  }
349
349
  }
350
350
 
351
+ func setRefreshAppearance(_ props: [String: Any]) {
352
+ nativeRefreshControl.tintColor = (props["tintColor"] as? String).flatMap { UIColor(hex: $0) }
353
+ }
354
+
351
355
  @objc private func refreshTriggered() {
352
356
  refreshHandler?()
353
357
  DispatchQueue.main.async { [weak self] in
@@ -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
@@ -3318,6 +3330,7 @@ private final class CraftNativeSvgView: UIView {
3318
3330
  var linejoin: String?
3319
3331
  var dashes: [CGFloat]?
3320
3332
  var vectorEffect: String?
3333
+ var fontWeight: String?
3321
3334
  var dominantBaseline: String?
3322
3335
 
3323
3336
  func merged(_ node: [String: Any]) -> Paint {
@@ -3330,6 +3343,11 @@ private final class CraftNativeSvgView: UIView {
3330
3343
  let value = CGFloat(truncating: number)
3331
3344
  return value.isFinite ? value : nil
3332
3345
  }
3346
+ func string(_ raw: Any?) -> String? {
3347
+ if let value = raw as? String { return value }
3348
+ if let number = raw as? NSNumber { return number.stringValue }
3349
+ return nil
3350
+ }
3333
3351
  if let color = style["color"] as? String { next.color = color }
3334
3352
  if let fill = style["fill"] as? String ?? props["fill"] as? String { next.fill = fill }
3335
3353
  if let stroke = style["stroke"] as? String ?? props["stroke"] as? String { next.stroke = stroke }
@@ -3346,6 +3364,7 @@ private final class CraftNativeSvgView: UIView {
3346
3364
  if let join = value("strokeLinejoin") as? String { next.linejoin = join }
3347
3365
  if let dashes = value("strokeDasharray") as? [Any] { next.dashes = dashes.compactMap(finite).filter { $0 >= 0 } }
3348
3366
  if let vectorEffect = value("vectorEffect") as? String { next.vectorEffect = vectorEffect }
3367
+ if let fontWeight = string(value("fontWeight")) { next.fontWeight = fontWeight }
3349
3368
  if let baseline = value("dominantBaseline") as? String { next.dominantBaseline = baseline }
3350
3369
  return next
3351
3370
  }
@@ -3446,7 +3465,14 @@ private final class CraftNativeSvgView: UIView {
3446
3465
  let props = node["props"] as? [String: Any] ?? [:]
3447
3466
  let style = node["style"] as? [String: Any] ?? [:]
3448
3467
  let size = number(props["fontSize"]) ?? number(style["fontSize"]) ?? 12
3449
- let weight: UIFont.Weight = (props["fontWeight"] as? String ?? style["fontWeight"] as? String).map { ["500": .medium, "600": .semibold, "700": .bold, "bold": .bold][$0] ?? .regular } ?? .regular
3468
+ let weight: UIFont.Weight = {
3469
+ switch paint.fontWeight?.lowercased() {
3470
+ case "500": return .medium
3471
+ case "600": return .semibold
3472
+ case "bold", "700", "800", "900": return .bold
3473
+ default: return .regular
3474
+ }
3475
+ }()
3450
3476
  let font = UIFont.systemFont(ofSize: size, weight: weight)
3451
3477
  let resolvedTextColor = paint.resolve(paint.fill) ?? UIColor.label
3452
3478
  let textAlpha = max(0, min(1, paint.opacity * paint.fillOpacity))
@@ -5779,6 +5805,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
5779
5805
  updateImage(image, source: props["source"], defaultSource: props["defaultSource"])
5780
5806
  case "ImageBackground":
5781
5807
  let background = result as! CraftNativeImageBackgroundView
5808
+ let backgroundID = ObjectIdentifier(background)
5809
+ if let tint = color(style["tintColor"]) {
5810
+ tintedImages.insert(backgroundID)
5811
+ background.tintColor = tint
5812
+ } else {
5813
+ tintedImages.remove(backgroundID)
5814
+ background.tintColor = nil
5815
+ }
5816
+ if let current = background.image { background.image = imageForDisplay(current, view: background) }
5782
5817
  let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
5783
5818
  background.contentMode = imageContentMode(requestedResizeMode)
5784
5819
  background.clipsToBounds = background.contentMode == .scaleAspectFill
@@ -6139,6 +6174,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6139
6174
  let owner = parent.protocolId ?? path
6140
6175
  flatListOwners = flatListOwners.filter { $0.value != owner }
6141
6176
  for child in children { registerFlatListOwnership(child, owner: owner) }
6177
+ let propRefresh = props["refreshControl"] as? [String: Any] ?? [:]
6178
+ let propRefreshEvents = propRefresh["events"] as? [String: String] ?? [:]
6179
+ let refreshHandler = nonEmptyHandler(propRefreshEvents["onRefresh"])
6180
+ ?? nonEmptyHandler(events["onRefresh"])
6181
+ let refreshing = (propRefresh["refreshing"] ?? props["refreshing"]) as? Bool == true
6142
6182
  list.bounces = props["bounces"] as? Bool ?? true
6143
6183
  list.isScrollEnabled = props["scrollEnabled"] as? Bool != false
6144
6184
  list.onFittingHeightChanged = { [weak list] in
@@ -6147,9 +6187,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6147
6187
  }
6148
6188
  list.setKeyboardShouldPersistTaps(props["keyboardShouldPersistTaps"])
6149
6189
  list.setContentContainerStyle(props["contentContainerStyle"] as? [String: Any])
6150
- list.setRefreshHandler(nonEmptyHandler(events["onRefresh"]).map { [weak self] handler in
6190
+ list.setRefreshHandler(refreshHandler.map { [weak self] handler in
6151
6191
  { self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
6152
- }, refreshing: props["refreshing"] as? Bool == true)
6192
+ }, refreshing: refreshing)
6193
+ list.setRefreshAppearance(propRefresh)
6153
6194
  list.keyboardDismissMode = keyboardDismissMode(props["keyboardDismissMode"])
6154
6195
  list.onScrollEvent = nonEmptyHandler(events["onScroll"]).map { handler in
6155
6196
  { [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
@@ -6581,7 +6622,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
6581
6622
  (icon as? CraftNativeIconView)?.defaultBox = box == nil && fontSize == nil ? 20 : nil
6582
6623
  let pointSize = fontSize ?? (box ?? 20) * 0.84
6583
6624
  let weight: UIImage.SymbolWeight
6584
- switch style["fontWeight"] as? String {
6625
+ switch craftNativeFontWeight(style["fontWeight"]) {
6585
6626
  case "100": weight = .ultraLight
6586
6627
  case "200": weight = .thin
6587
6628
  case "300": weight = .light
@@ -42,11 +42,14 @@ import XCTest
42
42
  /// ifid <id> <command…> runs the command only when that element is there (a screen shown the first time)
43
43
  /// log <text>
44
44
  ///
45
- /// Checks log `PASS …` / `FAIL …` lines into driver.log; matrix.sh counts them.
45
+ /// With a bundled journey.txt, evidence is kept as native XCTest attachments
46
+ /// and PASS/FAIL lines appear in the test log. Control-server journeys also
47
+ /// write driver.log; matrix.sh counts those checks.
46
48
  final class DriverTests: XCTestCase {
47
49
  let control = ProcessInfo.processInfo.environment["CRAFT_TEST_CONTROL"] ?? {{CONTROL_URL}}
48
50
  let app = XCUIApplication(bundleIdentifier: {{BUNDLE_ID}})
49
51
  let tabs: [String] = {{TABS}}
52
+ private var bundledJourney = false
50
53
 
51
54
  func get(_ path: String, timeout: Double = 15) -> Data? {
52
55
  var result: Data?
@@ -57,6 +60,15 @@ final class DriverTests: XCTestCase {
57
60
  }
58
61
 
59
62
  func post(_ path: String, _ body: Data) {
63
+ if bundledJourney {
64
+ guard path.hasPrefix("/save?"),
65
+ let name = URLComponents(string: path)?.queryItems?.first(where: { $0.name == "name" })?.value else { return }
66
+ let attachment = XCTAttachment(data: body, uniformTypeIdentifier: name.hasSuffix(".png") ? "public.png" : "public.plain-text")
67
+ attachment.name = name
68
+ attachment.lifetime = .keepAlways
69
+ add(attachment)
70
+ return
71
+ }
60
72
  var request = URLRequest(url: URL(string: control + path)!)
61
73
  request.httpMethod = "POST"
62
74
  request.httpBody = body
@@ -67,7 +79,7 @@ final class DriverTests: XCTestCase {
67
79
 
68
80
  func note(_ text: String) {
69
81
  print("DRIVER \(text)")
70
- post("/note", Data(text.utf8))
82
+ if !bundledJourney { post("/note", Data(text.utf8)) }
71
83
  }
72
84
 
73
85
  func check(_ ok: Bool, _ what: String) {
@@ -156,7 +168,14 @@ final class DriverTests: XCTestCase {
156
168
 
157
169
  func testScript() throws {
158
170
  continueAfterFailure = true
159
- guard let data = get("/script"), let script = String(data: data, encoding: .utf8) else {
171
+ let data: Data?
172
+ if let resource = Bundle(for: DriverTests.self).url(forResource: "journey", withExtension: "txt") {
173
+ bundledJourney = true
174
+ data = try Data(contentsOf: resource)
175
+ } else {
176
+ data = get("/script")
177
+ }
178
+ guard let data, let script = String(data: data, encoding: .utf8) else {
160
179
  XCTFail("no script")
161
180
  return
162
181
  }
@@ -171,6 +190,10 @@ final class DriverTests: XCTestCase {
171
190
  let parts = line.split(separator: " ", maxSplits: 1).map(String.init)
172
191
  let command = parts[0]
173
192
  let arg = parts.count > 1 ? parts[1] : ""
193
+ if bundledJourney && ["net", "delay", "exec"].contains(command) {
194
+ check(false, "\(command) requires a control-server journey")
195
+ return
196
+ }
174
197
  switch command {
175
198
  case "ifid":
176
199
  let rest = arg.split(separator: " ", maxSplits: 1).map(String.init)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.205",
3
+ "version": "0.0.207",
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>",