craft-native 0.0.247 → 0.0.248

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.
Files changed (41) hide show
  1. package/dist/android/src/index.d.ts +2 -0
  2. package/dist/android/src/index.js +22 -18
  3. package/dist/android/templates/CraftNative.kt.template +17 -0
  4. package/dist/android/templates/CraftNativeFlatList.kt.template +115 -40
  5. package/dist/android/templates/CraftNativeMapGoogle.kt.template +92 -5
  6. package/dist/android/templates/CraftNativeMapLibre.kt.template +90 -3
  7. package/dist/android/templates/CraftNativeMutation.kt.template +32 -0
  8. package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +7 -2
  9. package/dist/android/templates/CraftNativeServices.kt.template +25 -0
  10. package/dist/android/templates/CraftNativeShell.kt.template +34 -8
  11. package/dist/android/templates/MainActivityNative.kt.template +369 -69
  12. package/dist/android/templates/build.gradle.kts.project.template +1 -1
  13. package/dist/api/mobile.d.ts +37 -1
  14. package/dist/browser.js +9 -9
  15. package/dist/cli.js +90 -673
  16. package/dist/components/index.d.ts +50 -9
  17. package/dist/index.cjs +394 -438
  18. package/dist/index.d.cts +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +446 -493
  21. package/dist/ios/src/index.d.ts +2 -0
  22. package/dist/ios/src/index.js +54 -53
  23. package/dist/ios/templates/CraftApp.swift +37 -2
  24. package/dist/ios/templates/CraftHealth.swift +217 -0
  25. package/dist/ios/templates/CraftHeartRate.swift +6 -3
  26. package/dist/ios/templates/CraftHybrid.swift +18 -19
  27. package/dist/ios/templates/CraftLiveActivities.swift +1 -1
  28. package/dist/ios/templates/CraftNativeActions.swift +87 -8
  29. package/dist/ios/templates/CraftNativeFiles.swift +15 -3
  30. package/dist/ios/templates/CraftNativeFlatList.swift +18 -1
  31. package/dist/ios/templates/CraftNativeMutation.swift +36 -0
  32. package/dist/ios/templates/CraftNativeScreen.swift +813 -65
  33. package/dist/ios/templates/CraftNativeServices.swift +4 -2
  34. package/dist/ios/templates/CraftNativeUI.swift +10 -2
  35. package/dist/ios/templates/testing/DriverTests.swift.template +1 -0
  36. package/dist/ios/templates/testing/HostApp.swift +9 -2
  37. package/dist/macos/templates/CraftNativeMutation.swift +36 -0
  38. package/dist/macos/templates/native_layout.zig +226 -2
  39. package/dist/mobile-testing.js +2 -2
  40. package/dist/mobile.js +101 -63
  41. package/package.json +1 -1
@@ -313,8 +313,10 @@ final class CraftNativeSounds {
313
313
  func play(id: String, url: URL, options: [String: Any], answer: @escaping (Result<Any, CraftNativeActionError>) -> Void) {
314
314
  stop(id: id, fadeOut: 0)
315
315
  let loop = options["loop"] as? Bool ?? false
316
- let volume = Float(min(max((options["volume"] as? NSNumber)?.doubleValue ?? 1, 0), 1))
317
- let fadeIn = max(0, (options["fadeIn"] as? NSNumber)?.doubleValue ?? 0) / 1000
316
+ let volumeValue = craftNativeNumber(options["volume"]).map { $0.doubleValue }.flatMap { $0.isFinite ? $0 : nil } ?? 1
317
+ let fadeInValue = craftNativeNumber(options["fadeIn"]).map { $0.doubleValue }.flatMap { $0.isFinite ? $0 : nil } ?? 0
318
+ let volume = Float(min(max(volumeValue, 0), 1))
319
+ let fadeIn = max(0, fadeInValue) / 1000
318
320
  Self.claimSession(respectSilentSwitch: options["respectSilentSwitch"] as? Bool ?? true)
319
321
  let item = AVPlayerItem(url: url)
320
322
  let player = AVQueuePlayer()
@@ -1,4 +1,5 @@
1
1
  import AuthenticationServices
2
+ import CoreFoundation
2
3
  import SafariServices
3
4
  import UIKit
4
5
  import WebKit
@@ -54,7 +55,12 @@ enum CraftPresenter {
54
55
  /// view's points, as a rect. Nil when it is not one.
55
56
  static func rect(_ value: Any?) -> CGRect? {
56
57
  guard let value = value as? [String: Any] else { return nil }
57
- func number(_ key: String) -> CGFloat? { (value[key] as? NSNumber).map { CGFloat(truncating: $0) } }
58
+ func number(_ key: String) -> CGFloat? {
59
+ guard let number = value[key] as? NSNumber,
60
+ CFGetTypeID(number) != CFBooleanGetTypeID() else { return nil }
61
+ let value = CGFloat(truncating: number)
62
+ return value.isFinite ? value : nil
63
+ }
58
64
  guard let x = number("x"), let y = number("y") else { return nil }
59
65
  return CGRect(x: x, y: y, width: max(0, number("width") ?? 0), height: max(0, number("height") ?? 0))
60
66
  }
@@ -464,7 +470,9 @@ extension CraftSignInWithApple: ASAuthorizationControllerDelegate, ASAuthorizati
464
470
  /// show the same glyph the system's own controls use.
465
471
  enum CraftSymbols {
466
472
  static func image(named name: String, options: [String: Any]) -> String? {
467
- let pointSize = (options["pointSize"] as? NSNumber).map { CGFloat(truncating: $0) } ?? 17
473
+ let pointSize = craftNativeNumber(options["pointSize"])
474
+ .map { CGFloat(truncating: $0) }
475
+ .flatMap { $0.isFinite ? $0 : nil } ?? 17
468
476
  let weight: UIImage.SymbolWeight
469
477
  switch options["weight"] as? String {
470
478
  case "ultraLight": weight = .ultraLight
@@ -276,6 +276,7 @@ final class DriverTests: XCTestCase {
276
276
  let host = XCUIApplication(bundleIdentifier: "{{BUNDLE_PREFIX}}.host")
277
277
  host.launchArguments = ["--craft-universal-link", url.absoluteString]
278
278
  host.launch()
279
+ host.buttons["craft-driver-open-universal-link"].tap()
279
280
  let opened = app.wait(for: .runningForeground, timeout: 75)
280
281
  if !opened {
281
282
  post("/save?name=universal-link-host.txt", Data(host.debugDescription.utf8))
@@ -7,8 +7,15 @@ struct HostApp: App {
7
7
 
8
8
  var body: some Scene {
9
9
  WindowGroup {
10
- Text(status).accessibilityIdentifier("craft-driver-status")
11
- .task { await openUniversalLinkIfRequested() }
10
+ VStack {
11
+ Text(status).accessibilityIdentifier("craft-driver-status")
12
+ // XCTest must finish acquiring the host's launch assertion
13
+ // before a handoff can move another application foreground.
14
+ Button("Open Universal Link") {
15
+ Task { await openUniversalLinkIfRequested() }
16
+ }
17
+ .accessibilityIdentifier("craft-driver-open-universal-link")
18
+ }
12
19
  }
13
20
  }
14
21
 
@@ -108,6 +108,13 @@ final class CraftNativeMutationDocument {
108
108
  case "moveChild":
109
109
  try moveChild(operation, nodes: &candidateNodes)
110
110
  if let id = operation["parentId"] as? String { appendUnique(id, to: &affectedNodeIds) }
111
+ case "reparentChild":
112
+ let previousParent = try reparentChild(operation, nodes: &candidateNodes)
113
+ if let id = operation["parentId"] as? String { appendUnique(id, to: &affectedNodeIds) }
114
+ appendUnique(previousParent, to: &affectedNodeIds)
115
+ // A child crossing parents needs the committed tree to be
116
+ // reconciled so the host can move its existing native view.
117
+ requiresFullRender = true
111
118
  case "removeNode":
112
119
  if let id = operation["id"] as? String, let parent = candidateNodes[id]?.parent {
113
120
  appendUnique(parent, to: &affectedNodeIds)
@@ -251,6 +258,35 @@ final class CraftNativeMutationDocument {
251
258
  nodes[parentId] = parent
252
259
  }
253
260
 
261
+ private func reparentChild(_ operation: [String: Any], nodes: inout [String: Node]) throws -> String {
262
+ let parentId = try requiredId(operation, field: "parentId", name: "reparentChild")
263
+ let childId = try requiredId(operation, field: "childId", name: "reparentChild")
264
+ guard var parent = nodes[parentId] else {
265
+ throw CraftNativeMutationFailure("UNKNOWN_PARENT", "node \(parentId) does not exist")
266
+ }
267
+ guard var child = nodes[childId], let previousParentId = child.parent,
268
+ var previousParent = nodes[previousParentId] else {
269
+ throw CraftNativeMutationFailure("NOT_ATTACHED", "node \(childId) is not attached to a parent")
270
+ }
271
+ guard acceptsChildren(parent.type) else {
272
+ throw CraftNativeMutationFailure("INVALID_PARENT", "node \(parentId) cannot contain child nodes")
273
+ }
274
+ guard parentId != childId, !descendants(of: childId, nodes: nodes).contains(parentId) else {
275
+ throw CraftNativeMutationFailure("CYCLE", "reparenting node \(childId) would create a cycle")
276
+ }
277
+ guard parentId != previousParentId else {
278
+ throw CraftNativeMutationFailure("SAME_PARENT", "node \(childId) is already a child of \(parentId)")
279
+ }
280
+ let index = try insertionIndex(operation, count: parent.children.count)
281
+ previousParent.children.removeAll { $0 == childId }
282
+ parent.children.insert(childId, at: index)
283
+ child.parent = parentId
284
+ nodes[previousParentId] = previousParent
285
+ nodes[parentId] = parent
286
+ nodes[childId] = child
287
+ return previousParentId
288
+ }
289
+
254
290
  private func removeNode(
255
291
  _ operation: [String: Any],
256
292
  nodes: inout [String: Node],
@@ -105,6 +105,8 @@ pub const Style = struct {
105
105
  /// ABI and remains host-neutral because hosts resolve percentages before
106
106
  /// serializing the style.
107
107
  grid_auto_rows: ?f32 = null,
108
+ /// Explicit equal row-track count. Null means derive rows from children.
109
+ grid_rows: ?u32 = null,
108
110
  };
109
111
 
110
112
  pub const Node = struct {
@@ -297,6 +299,51 @@ pub const LayoutNodeInputV4 = extern struct {
297
299
  child_count: u32,
298
300
  };
299
301
 
302
+ /// v5 appends the resolved explicit grid-row count to the v4 payload.
303
+ pub const LayoutStyleInputV5 = extern struct {
304
+ direction: u32,
305
+ width: f32,
306
+ height: f32,
307
+ intrinsic_width: f32,
308
+ intrinsic_height: f32,
309
+ baseline: f32,
310
+ aspect_ratio: f32,
311
+ flex_basis: f32,
312
+ flex_grow: f32,
313
+ flex_shrink: f32,
314
+ min_width: f32,
315
+ max_width: f32,
316
+ min_height: f32,
317
+ max_height: f32,
318
+ gap: f32,
319
+ justify_content: u32,
320
+ align_items: u32,
321
+ align_self: u32,
322
+ flex_wrap: u32,
323
+ align_content: u32,
324
+ margin_top: f32,
325
+ margin_right: f32,
326
+ margin_bottom: f32,
327
+ margin_left: f32,
328
+ padding_top: f32,
329
+ padding_right: f32,
330
+ padding_bottom: f32,
331
+ padding_left: f32,
332
+ grid: u32,
333
+ grid_columns: u32,
334
+ row_gap: f32,
335
+ column_gap: f32,
336
+ reverse: u32,
337
+ grid_auto_rows: f32,
338
+ grid_rows: u32,
339
+ };
340
+
341
+ pub const LayoutNodeInputV5 = extern struct {
342
+ style: LayoutStyleInputV5,
343
+ first_child: u32,
344
+ child_count: u32,
345
+ };
346
+
300
347
  const max_layout_nodes: usize = 65_536;
301
348
 
302
349
  /// Resolve an entire native view tree using the same layout rules used by the
@@ -452,6 +499,43 @@ pub export fn craft_native_layout_tree_v4(
452
499
  return if (result == node_count) 0 else -1;
453
500
  }
454
501
 
502
+ /// v5 tree layout entry point. It preserves v4's auto-row and reverse fields,
503
+ /// adding an explicit equal-track row count for gridTemplateRows.
504
+ pub export fn craft_native_layout_tree_v5(
505
+ viewport_width: f32,
506
+ viewport_height: f32,
507
+ nodes: [*]const LayoutNodeInputV5,
508
+ node_count: usize,
509
+ frames: [*]Frame,
510
+ frame_count: usize,
511
+ ) callconv(.c) c_int {
512
+ if (node_count == 0 or node_count > max_layout_nodes or frame_count < node_count) return -1;
513
+
514
+ const input = nodes[0..node_count];
515
+ const visited = std.heap.page_allocator.alloc(bool, node_count) catch return -1;
516
+ defer std.heap.page_allocator.free(visited);
517
+ @memset(visited, false);
518
+ if (!validateLayoutTreeV5(input, 0, visited)) return -1;
519
+
520
+ var tree = std.heap.page_allocator.alloc(Node, node_count) catch return -1;
521
+ defer std.heap.page_allocator.free(tree);
522
+ for (input, 0..) |item, index| {
523
+ const first = @as(usize, @intCast(item.first_child));
524
+ const count = @as(usize, @intCast(item.child_count));
525
+ tree[index] = .{
526
+ .style = layoutStyleV5(item.style),
527
+ .children = tree[first .. first + count],
528
+ };
529
+ }
530
+
531
+ const result = layout(
532
+ tree[0],
533
+ .{ .width = finiteNonNegative(viewport_width), .height = finiteNonNegative(viewport_height) },
534
+ frames[0..frame_count],
535
+ ) catch return -1;
536
+ return if (result == node_count) 0 else -1;
537
+ }
538
+
455
539
  fn validateLayoutTree(input: []const LayoutNodeInput, index: usize, visited: []bool) bool {
456
540
  if (index >= input.len or visited[index]) return false;
457
541
  visited[index] = true;
@@ -491,6 +575,19 @@ fn validateLayoutTreeV4(input: []const LayoutNodeInputV4, index: usize, visited:
491
575
  return true;
492
576
  }
493
577
 
578
+ fn validateLayoutTreeV5(input: []const LayoutNodeInputV5, index: usize, visited: []bool) bool {
579
+ if (index >= input.len or visited[index]) return false;
580
+ visited[index] = true;
581
+ const item = input[index];
582
+ const first = @as(usize, @intCast(item.first_child));
583
+ const count = @as(usize, @intCast(item.child_count));
584
+ if (first > input.len or count > input.len - first or (count > 0 and first <= index)) return false;
585
+ for (first..first + count) |child| {
586
+ if (!validateLayoutTreeV5(input, child, visited)) return false;
587
+ }
588
+ return true;
589
+ }
590
+
494
591
  fn layoutStyle(input: LayoutStyleInput) Style {
495
592
  return .{
496
593
  .direction = if (input.direction == 1) .column else .row,
@@ -614,6 +711,47 @@ fn layoutStyleV4(input: LayoutStyleInputV4) Style {
614
711
  };
615
712
  }
616
713
 
714
+ fn layoutStyleV5(input: LayoutStyleInputV5) Style {
715
+ return .{
716
+ .direction = if (input.direction == 1) .column else .row,
717
+ .reverse = (input.reverse & 1) != 0,
718
+ .wrap_reverse = (input.reverse & 2) != 0,
719
+ .width = optionalFinite(input.width),
720
+ .height = optionalFinite(input.height),
721
+ .intrinsic_width = optionalFinite(input.intrinsic_width),
722
+ .intrinsic_height = optionalFinite(input.intrinsic_height),
723
+ .baseline = optionalFinite(input.baseline),
724
+ .aspect_ratio = optionalFinite(input.aspect_ratio),
725
+ .flex_basis = optionalFinite(input.flex_basis),
726
+ .flex_grow = finiteNonNegative(input.flex_grow),
727
+ .flex_shrink = finiteNonNegative(input.flex_shrink),
728
+ .min_width = optionalFinite(input.min_width),
729
+ .max_width = optionalFinite(input.max_width),
730
+ .min_height = optionalFinite(input.min_height),
731
+ .max_height = optionalFinite(input.max_height),
732
+ .gap = finiteNonNegative(input.gap),
733
+ .justify_content = @fromBackingInt(@intCast(@min(input.justify_content, 5))),
734
+ .align_items = @fromBackingInt(@intCast(@min(input.align_items, 4))),
735
+ .align_self = if (input.align_self == 0) null else @fromBackingInt(@intCast(@min(input.align_self - 1, 4))),
736
+ .flex_wrap = input.flex_wrap != 0,
737
+ .align_content = @fromBackingInt(@intCast(@min(input.align_content, 6))),
738
+ .margin_top = finiteNonNegative(input.margin_top),
739
+ .margin_right = finiteNonNegative(input.margin_right),
740
+ .margin_bottom = finiteNonNegative(input.margin_bottom),
741
+ .margin_left = finiteNonNegative(input.margin_left),
742
+ .padding_top = finiteNonNegative(input.padding_top),
743
+ .padding_right = finiteNonNegative(input.padding_right),
744
+ .padding_bottom = finiteNonNegative(input.padding_bottom),
745
+ .padding_left = finiteNonNegative(input.padding_left),
746
+ .grid = input.grid != 0,
747
+ .grid_columns = @max(@as(u32, 1), input.grid_columns),
748
+ .row_gap = finiteNonNegative(input.row_gap),
749
+ .column_gap = finiteNonNegative(input.column_gap),
750
+ .grid_auto_rows = optionalFinite(input.grid_auto_rows),
751
+ .grid_rows = if (input.grid_rows == 0) null else input.grid_rows,
752
+ };
753
+ }
754
+
617
755
  fn optionalFinite(value: f32) ?f32 {
618
756
  return if (std.math.isFinite(value)) value else null;
619
757
  }
@@ -1359,12 +1497,26 @@ fn layoutGrid(
1359
1497
  const row_gap = axisGap(node.style.row_gap, node.style.gap);
1360
1498
  const available_width = @max(0, width - column_gap * @as(f32, @floatFromInt(column_count - 1)));
1361
1499
  const cell_width = available_width / @as(f32, @floatFromInt(column_count));
1500
+ const natural_rows = if (node.children.len == 0) 0 else (node.children.len + column_count - 1) / column_count;
1501
+ const row_count = @max(natural_rows, @as(usize, @intCast(node.style.grid_rows orelse 0)));
1502
+ const row_heights = std.heap.page_allocator.alloc(f32, row_count) catch return error.FrameBufferTooSmall;
1503
+ defer std.heap.page_allocator.free(row_heights);
1504
+ for (0..row_count) |row| {
1505
+ var track = gridRowHeight(node, row, column_count);
1506
+ if (node.style.grid_rows != null) {
1507
+ const gap_count = if (row_count > 1) row_count - 1 else 0;
1508
+ const gaps = row_gap * @as(f32, @floatFromInt(gap_count));
1509
+ const equal = @max(0, (height - gaps) / @as(f32, @floatFromInt(@max(row_count, 1))));
1510
+ track = @max(track, equal);
1511
+ }
1512
+ row_heights[row] = track;
1513
+ }
1362
1514
  var next_index = frame_index + 1;
1363
1515
  var row_y = y;
1364
1516
  for (node.children, 0..) |child, index| {
1365
1517
  const row = index / column_count;
1366
1518
  const column = index % column_count;
1367
- const row_height = gridRowHeight(node, row, column_count);
1519
+ const row_height = row_heights[row];
1368
1520
  const child_available_width = @max(0, cell_width - horizontalMargins(child));
1369
1521
  const ratio_width = if (validAspectRatio(child.style.aspect_ratio)) |ratio|
1370
1522
  if (child.style.width == null and child.style.height != null) child.style.height.? * ratio else null
@@ -1395,7 +1547,6 @@ fn layoutGrid(
1395
1547
  row_y += row_height + row_gap;
1396
1548
  }
1397
1549
  }
1398
- _ = height;
1399
1550
  return next_index;
1400
1551
  }
1401
1552
 
@@ -1708,6 +1859,47 @@ fn emptyLayoutStyleInputV4() LayoutStyleInputV4 {
1708
1859
  };
1709
1860
  }
1710
1861
 
1862
+ fn emptyLayoutStyleInputV5() LayoutStyleInputV5 {
1863
+ const unset = std.math.nan(f32);
1864
+ return .{
1865
+ .direction = 0,
1866
+ .width = unset,
1867
+ .height = unset,
1868
+ .intrinsic_width = unset,
1869
+ .intrinsic_height = unset,
1870
+ .baseline = unset,
1871
+ .aspect_ratio = unset,
1872
+ .flex_basis = unset,
1873
+ .flex_grow = 0,
1874
+ .flex_shrink = 0,
1875
+ .min_width = unset,
1876
+ .max_width = unset,
1877
+ .min_height = unset,
1878
+ .max_height = unset,
1879
+ .gap = 0,
1880
+ .justify_content = 0,
1881
+ .align_items = 3,
1882
+ .align_self = 0,
1883
+ .flex_wrap = 0,
1884
+ .align_content = 5,
1885
+ .margin_top = 0,
1886
+ .margin_right = 0,
1887
+ .margin_bottom = 0,
1888
+ .margin_left = 0,
1889
+ .padding_top = 0,
1890
+ .padding_right = 0,
1891
+ .padding_bottom = 0,
1892
+ .padding_left = 0,
1893
+ .grid = 0,
1894
+ .grid_columns = 1,
1895
+ .row_gap = 0,
1896
+ .column_gap = 0,
1897
+ .reverse = 0,
1898
+ .grid_auto_rows = unset,
1899
+ .grid_rows = 0,
1900
+ };
1901
+ }
1902
+
1711
1903
  fn clampBounds(value: f32, lower: f32, upper: f32) f32 {
1712
1904
  var result = finiteNonNegative(value);
1713
1905
  if (std.math.isFinite(lower) and lower >= 0) result = @max(result, lower);
@@ -1818,6 +2010,38 @@ test "v4 tree layout carries auto rows through nested grids" {
1818
2010
  try std.testing.expectEqual(@as(f32, 18), frames[4].y);
1819
2011
  }
1820
2012
 
2013
+ test "v5 tree layout carries explicit equal grid rows through the shared ABI" {
2014
+ var root_style = emptyLayoutStyleInputV5();
2015
+ root_style.width = 100;
2016
+ root_style.height = 100;
2017
+ root_style.grid = 1;
2018
+ root_style.grid_columns = 2;
2019
+ root_style.row_gap = 4;
2020
+ root_style.grid_rows = 2;
2021
+
2022
+ var first_style = emptyLayoutStyleInputV5();
2023
+ first_style.intrinsic_width = 10;
2024
+ first_style.intrinsic_height = 6;
2025
+ var second_style = emptyLayoutStyleInputV5();
2026
+ second_style.intrinsic_width = 10;
2027
+ second_style.intrinsic_height = 7;
2028
+ var third_style = emptyLayoutStyleInputV5();
2029
+ third_style.intrinsic_width = 10;
2030
+ third_style.intrinsic_height = 5;
2031
+
2032
+ const nodes = [_]LayoutNodeInputV5{
2033
+ .{ .style = root_style, .first_child = 1, .child_count = 3 },
2034
+ .{ .style = first_style, .first_child = 0, .child_count = 0 },
2035
+ .{ .style = second_style, .first_child = 0, .child_count = 0 },
2036
+ .{ .style = third_style, .first_child = 0, .child_count = 0 },
2037
+ };
2038
+ var frames: [4]Frame = undefined;
2039
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_layout_tree_v5(100, 100, &nodes, nodes.len, &frames, frames.len));
2040
+ try std.testing.expectEqual(@as(f32, 0), frames[1].y);
2041
+ try std.testing.expectEqual(@as(f32, 0), frames[2].y);
2042
+ try std.testing.expectEqual(@as(f32, 52), frames[3].y);
2043
+ }
2044
+
1821
2045
  test "lays out nested flat trees with absolute descendant frames" {
1822
2046
  var root_style = emptyLayoutStyleInput();
1823
2047
  root_style.width = 200;
@@ -477,7 +477,7 @@ ${errors || output}`);
477
477
  return { restart, installUpdate, installedBuild };
478
478
  }
479
479
  export {
480
- verifyMobileMediaCache,
480
+ createMobileTestDevice,
481
481
  recordIosSimulatorVideo,
482
- createMobileTestDevice
482
+ verifyMobileMediaCache
483
483
  };
package/dist/mobile.js CHANGED
@@ -1145,9 +1145,9 @@ function hrSubscribe(set, callback) {
1145
1145
  if (reading)
1146
1146
  hrNotify(heartRateWatch.readings, reading);
1147
1147
  } else if (detail.type === "device") {
1148
- const device2 = normalizeHeartRateDevice(detail.device);
1149
- if (device2)
1150
- hrNotify(heartRateWatch.devices, device2);
1148
+ const device = normalizeHeartRateDevice(detail.device);
1149
+ if (device)
1150
+ hrNotify(heartRateWatch.devices, device);
1151
1151
  } else if (detail.type === "state") {
1152
1152
  hrNotify(heartRateWatch.states, normalizeHeartRateState(detail));
1153
1153
  }
@@ -1184,7 +1184,7 @@ var heartRate = {
1184
1184
  requestAuthorization: () => heartRateCall("requestAuthorization", () => "unavailable", (value) => HR_AUTHORIZATIONS.includes(value) ? value : "unavailable"),
1185
1185
  scan: (options = {}) => heartRateCall("scan", noHeartRate, normalizeHeartRateState, options),
1186
1186
  stopScan: () => heartRateCall("stopScan", noHeartRate, normalizeHeartRateState),
1187
- getDevices: () => heartRateCall("getDevices", () => [], (value) => Array.isArray(value) ? value.map(normalizeHeartRateDevice).filter((device2) => device2 !== null) : []),
1187
+ getDevices: () => heartRateCall("getDevices", () => [], (value) => Array.isArray(value) ? value.map(normalizeHeartRateDevice).filter((device) => device !== null) : []),
1188
1188
  connect: (id) => heartRateCall("connect", noHeartRate, normalizeHeartRateState, id),
1189
1189
  reconnect: () => heartRateCall("reconnect", noHeartRate, normalizeHeartRateState),
1190
1190
  disconnect: (options = {}) => heartRateCall("disconnect", noHeartRate, normalizeHeartRateState, options),
@@ -1459,14 +1459,17 @@ var hybrid = {
1459
1459
  return postHybrid({ type: "back" });
1460
1460
  }
1461
1461
  };
1462
- async function callNative(namespace, method, args, fallback) {
1462
+ async function callNative(namespace, method, args, fallback, options = {}) {
1463
1463
  const target = getCraftRoot()?.[namespace];
1464
1464
  if (!target || typeof target[method] !== "function")
1465
1465
  return fallback;
1466
1466
  try {
1467
1467
  const value = await target[method](...args);
1468
1468
  return value === undefined || value === null ? fallback : value;
1469
- } catch {
1469
+ } catch (error) {
1470
+ if (options.warnOnError) {
1471
+ console.warn(`[craft] Native ${namespace}.${method} failed; using its fallback value.`, error);
1472
+ }
1470
1473
  return fallback;
1471
1474
  }
1472
1475
  }
@@ -1484,7 +1487,7 @@ var dialog = {
1484
1487
  return callNative("dialog", "confirm", [options], false);
1485
1488
  },
1486
1489
  actionSheet(options) {
1487
- return callNative("dialog", "actionSheet", [options], null);
1490
+ return callNative("dialog", "actionSheet", [options], null, { warnOnError: true });
1488
1491
  }
1489
1492
  };
1490
1493
  var contextMenu = {
@@ -1669,11 +1672,11 @@ var health = {
1669
1672
  throw new Error("Reading sleep intervals is unavailable");
1670
1673
  return craft.health.getSleepIntervals(options);
1671
1674
  },
1672
- async saveMindfulSession(session2) {
1675
+ async saveMindfulSession(session) {
1673
1676
  const craft = getCraftRoot();
1674
1677
  if (!craft?.health?.saveMindfulSession)
1675
1678
  throw new Error("Saving mindful sessions is unavailable");
1676
- return craft.health.saveMindfulSession(session2);
1679
+ return craft.health.saveMindfulSession(session);
1677
1680
  },
1678
1681
  async getMindfulSessions(options = {}) {
1679
1682
  const craft = getCraftRoot();
@@ -1693,6 +1696,41 @@ var health = {
1693
1696
  throw new Error("Heart-rate updates are unavailable");
1694
1697
  return Boolean(await craft.health.stopHeartRateUpdates());
1695
1698
  },
1699
+ async startWorkout(options) {
1700
+ const craft = getCraftRoot();
1701
+ if (!craft?.health?.startWorkout)
1702
+ throw new Error("Live workouts are unavailable");
1703
+ return craft.health.startWorkout(options);
1704
+ },
1705
+ async pauseWorkout() {
1706
+ const craft = getCraftRoot();
1707
+ if (!craft?.health?.pauseWorkout)
1708
+ throw new Error("Live workouts are unavailable");
1709
+ return Boolean(await craft.health.pauseWorkout());
1710
+ },
1711
+ async resumeWorkout() {
1712
+ const craft = getCraftRoot();
1713
+ if (!craft?.health?.resumeWorkout)
1714
+ throw new Error("Live workouts are unavailable");
1715
+ return Boolean(await craft.health.resumeWorkout());
1716
+ },
1717
+ async endWorkout() {
1718
+ const craft = getCraftRoot();
1719
+ if (!craft?.health?.endWorkout)
1720
+ throw new Error("Live workouts are unavailable");
1721
+ return craft.health.endWorkout();
1722
+ },
1723
+ onWorkout(callback) {
1724
+ if (typeof window === "undefined" || typeof getCraftRoot()?.health?.startWorkout !== "function")
1725
+ return () => {};
1726
+ const listener = (event) => {
1727
+ const detail = event.detail;
1728
+ if (detail && typeof detail.id === "string" && (detail.event === "state" || detail.event === "error"))
1729
+ callback(detail);
1730
+ };
1731
+ window.addEventListener("craftHealthWorkout", listener);
1732
+ return () => window.removeEventListener("craftHealthWorkout", listener);
1733
+ },
1696
1734
  onHeartRate(callback) {
1697
1735
  if (typeof window === "undefined" || typeof getCraftRoot()?.health?.startHeartRateUpdates !== "function")
1698
1736
  return () => {};
@@ -1819,59 +1857,59 @@ function getCraftMobile() {
1819
1857
  return;
1820
1858
  }
1821
1859
  export {
1822
- watchConnectivity,
1823
- tabBar,
1824
- symbols,
1825
- storage,
1826
- statusBar,
1827
- splash,
1828
- speech,
1829
- snapshots,
1830
- share,
1831
- session,
1832
- secureStorage,
1833
- scheduledWorkoutsUnsupported,
1834
- scheduledWorkouts,
1835
- refresh,
1836
- pushNotifications,
1837
- permissions,
1838
- notifications,
1839
- normalizeScheduledWorkout,
1840
- normalizeNowPlaying,
1841
- normalizeHeartRateState,
1842
- normalizeHeartRateSamples,
1843
- normalizeHeartRateReading,
1844
- normalizeHeartRateDevice,
1845
- normalizeDeepLinkURL,
1846
- network,
1847
- music,
1848
- location,
1849
- liveActivities,
1850
- lifecycle,
1851
- keepAwake,
1852
- isSnapshotName,
1853
- hybrid,
1854
- heartRate,
1855
- health,
1856
- haptics,
1857
- dialog,
1858
- device,
1859
- deepLinks,
1860
- crypto,
1861
- crashReporter,
1862
- contextMenu,
1863
- chrome,
1864
- camera,
1865
- browser,
1866
- brightness,
1867
- bridgeNotification,
1868
- biometrics,
1869
- background,
1870
- appearance,
1871
- appReview,
1872
- _webFallbackDeviceId,
1873
- _resetWebFallbackDeviceIdForTests,
1874
- SESSION_MESSAGE,
1860
+ DEFAULT_SESSION_TOKEN_KEY,
1875
1861
  SESSION_EVENT,
1876
- DEFAULT_SESSION_TOKEN_KEY
1862
+ SESSION_MESSAGE,
1863
+ _resetWebFallbackDeviceIdForTests,
1864
+ _webFallbackDeviceId,
1865
+ appReview,
1866
+ appearance,
1867
+ background,
1868
+ biometrics,
1869
+ bridgeNotification,
1870
+ brightness,
1871
+ browser,
1872
+ camera,
1873
+ chrome,
1874
+ contextMenu,
1875
+ crashReporter,
1876
+ crypto,
1877
+ deepLinks,
1878
+ device,
1879
+ dialog,
1880
+ haptics,
1881
+ health,
1882
+ heartRate,
1883
+ hybrid,
1884
+ isSnapshotName,
1885
+ keepAwake,
1886
+ lifecycle,
1887
+ liveActivities,
1888
+ location,
1889
+ music,
1890
+ network,
1891
+ normalizeDeepLinkURL,
1892
+ normalizeHeartRateDevice,
1893
+ normalizeHeartRateReading,
1894
+ normalizeHeartRateSamples,
1895
+ normalizeHeartRateState,
1896
+ normalizeNowPlaying,
1897
+ normalizeScheduledWorkout,
1898
+ notifications,
1899
+ permissions,
1900
+ pushNotifications,
1901
+ refresh,
1902
+ scheduledWorkouts,
1903
+ scheduledWorkoutsUnsupported,
1904
+ secureStorage,
1905
+ session,
1906
+ share,
1907
+ snapshots,
1908
+ speech,
1909
+ splash,
1910
+ statusBar,
1911
+ storage,
1912
+ symbols,
1913
+ tabBar,
1914
+ watchConnectivity
1877
1915
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.247",
3
+ "version": "0.0.248",
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>",