craft-native 0.0.175 → 0.0.177

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 (34) hide show
  1. package/dist/android/src/index.d.ts +19 -0
  2. package/dist/android/src/index.js +19 -2
  3. package/dist/android/templates/AndroidManifest.xml.template +1 -0
  4. package/dist/android/templates/CraftBridge.kt.template +51 -0
  5. package/dist/android/templates/CraftCrashReporter.kt.template +544 -0
  6. package/dist/android/templates/CraftNative.kt.template +17 -0
  7. package/dist/android/templates/CraftNativeFlatList.kt.template +20 -9
  8. package/dist/android/templates/CraftNativeMutation.kt.template +4 -1
  9. package/dist/android/templates/CraftNativeServices.kt.template +31 -1
  10. package/dist/android/templates/MainActivityNative.kt.template +986 -304
  11. package/dist/android/templates/build.gradle.kts.app.template +4 -2
  12. package/dist/api/mobile.d.ts +31 -0
  13. package/dist/cli.js +1 -1
  14. package/dist/components/index.d.ts +203 -2
  15. package/dist/index.cjs +1 -1
  16. package/dist/index.d.cts +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/ios/src/index.d.ts +19 -0
  20. package/dist/ios/src/index.js +7 -1
  21. package/dist/ios/src/testing.d.ts +5 -0
  22. package/dist/ios/templates/CraftApp.swift +51 -1
  23. package/dist/ios/templates/CraftCrashReporter.swift +1010 -0
  24. package/dist/ios/templates/CraftHybrid.swift +10 -0
  25. package/dist/ios/templates/CraftNativeActions.swift +49 -1
  26. package/dist/ios/templates/CraftNativeFlatList.swift +30 -28
  27. package/dist/ios/templates/CraftNativeMutation.swift +1 -1
  28. package/dist/ios/templates/CraftNativeScreen.swift +559 -86
  29. package/dist/ios/templates/project.yml.template +1 -0
  30. package/dist/ios/templates/testing/DriverTests.swift.template +8 -0
  31. package/dist/macos/templates/CraftNativeMutation.swift +1 -1
  32. package/dist/macos/templates/native_layout.zig +389 -17
  33. package/dist/mobile.js +66 -0
  34. package/package.json +1 -1
@@ -34,6 +34,7 @@ targets:
34
34
  settings:
35
35
  INFOPLIST_FILE: Info.plist
36
36
  CODE_SIGN_ENTITLEMENTS: Craft.entitlements
37
+ PRODUCT_NAME: "$(TARGET_NAME)"
37
38
  PRODUCT_BUNDLE_IDENTIFIER: {{BUNDLE_ID}}
38
39
  TARGETED_DEVICE_FAMILY: "{{DEVICE_FAMILIES}}"
39
40
  SWIFT_VERSION: "5.0"
@@ -286,6 +286,14 @@ final class DriverTests: XCTestCase {
286
286
  case "left": app.swipeLeft()
287
287
  default: app.swipeRight()
288
288
  }
289
+ case "edge":
290
+ // The system back-swipe: a drag in from the left edge (or the
291
+ // right one with `edge right`), which a centred swipe never is.
292
+ let fromRight = arg == "right"
293
+ let start = app.coordinate(withNormalizedOffset: CGVector(dx: fromRight ? 0.995 : 0.005, dy: 0.5))
294
+ let end = app.coordinate(withNormalizedOffset: CGVector(dx: fromRight ? 0.15 : 0.85, dy: 0.5))
295
+ start.press(forDuration: 0.05, thenDragTo: end)
296
+ note("edge \(fromRight ? "right" : "left"): ok")
289
297
  case "audit":
290
298
  var issues: [String] = []
291
299
  do {
@@ -305,7 +305,7 @@ final class CraftNativeMutationDocument {
305
305
  static let containerTypes: Set<String> = [
306
306
  "View", "SafeAreaView", "ScrollView", "KeyboardAvoidingView", "Modal", "Pressable",
307
307
  "TouchableOpacity", "TouchableHighlight", "ImageBackground", "FlatList", "SectionList",
308
- "Fragment", "Svg", "G",
308
+ "Fragment", "Svg", "G", "Map",
309
309
  ]
310
310
  private static let containerTypeKeys = Set(containerTypes.map { $0.lowercased() })
311
311
 
@@ -147,6 +147,152 @@ pub const GridItem = extern struct {
147
147
  alignment: u32,
148
148
  };
149
149
 
150
+ /// Flat, C-compatible input for the host-neutral tree layout entry point.
151
+ ///
152
+ /// Optional lengths use NaN as their unset value. Enum fields intentionally
153
+ /// use the same zero-based values as the public `Style` enums, except for
154
+ /// `align_self`: zero means `auto` and values one through five map to the
155
+ /// corresponding `AlignItems` value. Children are a contiguous preorder slice
156
+ /// described by `first_child` and `child_count`, so a host can build the tree
157
+ /// without allocating platform objects in the shared core.
158
+ pub const LayoutStyleInput = extern struct {
159
+ direction: u32,
160
+ width: f32,
161
+ height: f32,
162
+ intrinsic_width: f32,
163
+ intrinsic_height: f32,
164
+ baseline: f32,
165
+ aspect_ratio: f32,
166
+ flex_basis: f32,
167
+ flex_grow: f32,
168
+ flex_shrink: f32,
169
+ min_width: f32,
170
+ max_width: f32,
171
+ min_height: f32,
172
+ max_height: f32,
173
+ gap: f32,
174
+ justify_content: u32,
175
+ align_items: u32,
176
+ align_self: u32,
177
+ flex_wrap: u32,
178
+ align_content: u32,
179
+ margin_top: f32,
180
+ margin_right: f32,
181
+ margin_bottom: f32,
182
+ margin_left: f32,
183
+ padding_top: f32,
184
+ padding_right: f32,
185
+ padding_bottom: f32,
186
+ padding_left: f32,
187
+ grid: u32,
188
+ grid_columns: u32,
189
+ row_gap: f32,
190
+ column_gap: f32,
191
+ };
192
+
193
+ pub const LayoutNodeInput = extern struct {
194
+ style: LayoutStyleInput,
195
+ first_child: u32,
196
+ child_count: u32,
197
+ };
198
+
199
+ const max_layout_nodes: usize = 65_536;
200
+
201
+ /// Resolve an entire native view tree using the same layout rules used by the
202
+ /// host-neutral tests. Hosts may keep their platform measurement pass, flatten
203
+ /// the measured tree into `LayoutNodeInput`, and then apply these absolute
204
+ /// frames to UIKit or Android views. A non-zero result means the host should
205
+ /// retain its compatibility fallback; no partial frame array is considered
206
+ /// valid.
207
+ pub export fn craft_native_layout_tree(
208
+ viewport_width: f32,
209
+ viewport_height: f32,
210
+ nodes: [*]const LayoutNodeInput,
211
+ node_count: usize,
212
+ frames: [*]Frame,
213
+ frame_count: usize,
214
+ ) callconv(.c) c_int {
215
+ if (node_count == 0 or node_count > max_layout_nodes or frame_count < node_count) return -1;
216
+
217
+ const input = nodes[0..node_count];
218
+ const visited = std.heap.page_allocator.alloc(bool, node_count) catch return -1;
219
+ defer std.heap.page_allocator.free(visited);
220
+ @memset(visited, false);
221
+ if (!validateLayoutTree(input, 0, visited)) return -1;
222
+
223
+ var tree = std.heap.page_allocator.alloc(Node, node_count) catch return -1;
224
+ defer std.heap.page_allocator.free(tree);
225
+ for (input, 0..) |item, index| {
226
+ const first = @as(usize, @intCast(item.first_child));
227
+ const count = @as(usize, @intCast(item.child_count));
228
+ tree[index] = .{
229
+ .style = layoutStyle(item.style),
230
+ .children = tree[first .. first + count],
231
+ };
232
+ }
233
+
234
+ const result = layout(
235
+ tree[0],
236
+ .{ .width = finiteNonNegative(viewport_width), .height = finiteNonNegative(viewport_height) },
237
+ frames[0..frame_count],
238
+ ) catch return -1;
239
+ return if (result == node_count) 0 else -1;
240
+ }
241
+
242
+ fn validateLayoutTree(input: []const LayoutNodeInput, index: usize, visited: []bool) bool {
243
+ if (index >= input.len or visited[index]) return false;
244
+ visited[index] = true;
245
+ const item = input[index];
246
+ const first = @as(usize, @intCast(item.first_child));
247
+ const count = @as(usize, @intCast(item.child_count));
248
+ if (first > input.len or count > input.len - first or (count > 0 and first <= index)) return false;
249
+ for (first..first + count) |child| {
250
+ if (!validateLayoutTree(input, child, visited)) return false;
251
+ }
252
+ return true;
253
+ }
254
+
255
+ fn layoutStyle(input: LayoutStyleInput) Style {
256
+ return .{
257
+ .direction = if (input.direction == 1) .column else .row,
258
+ .width = optionalFinite(input.width),
259
+ .height = optionalFinite(input.height),
260
+ .intrinsic_width = optionalFinite(input.intrinsic_width),
261
+ .intrinsic_height = optionalFinite(input.intrinsic_height),
262
+ .baseline = optionalFinite(input.baseline),
263
+ .aspect_ratio = optionalFinite(input.aspect_ratio),
264
+ .flex_basis = optionalFinite(input.flex_basis),
265
+ .flex_grow = finiteNonNegative(input.flex_grow),
266
+ .flex_shrink = finiteNonNegative(input.flex_shrink),
267
+ .min_width = optionalFinite(input.min_width),
268
+ .max_width = optionalFinite(input.max_width),
269
+ .min_height = optionalFinite(input.min_height),
270
+ .max_height = optionalFinite(input.max_height),
271
+ .gap = finiteNonNegative(input.gap),
272
+ .justify_content = @fromBackingInt(@intCast(@min(input.justify_content, 5))),
273
+ .align_items = @fromBackingInt(@intCast(@min(input.align_items, 4))),
274
+ .align_self = if (input.align_self == 0) null else @fromBackingInt(@intCast(@min(input.align_self - 1, 4))),
275
+ .flex_wrap = input.flex_wrap != 0,
276
+ .align_content = @fromBackingInt(@intCast(@min(input.align_content, 6))),
277
+ .margin_top = finiteNonNegative(input.margin_top),
278
+ .margin_right = finiteNonNegative(input.margin_right),
279
+ .margin_bottom = finiteNonNegative(input.margin_bottom),
280
+ .margin_left = finiteNonNegative(input.margin_left),
281
+ .padding_top = finiteNonNegative(input.padding_top),
282
+ .padding_right = finiteNonNegative(input.padding_right),
283
+ .padding_bottom = finiteNonNegative(input.padding_bottom),
284
+ .padding_left = finiteNonNegative(input.padding_left),
285
+ .grid = input.grid != 0,
286
+ .grid_columns = @max(@as(u32, 1), input.grid_columns),
287
+ .row_gap = finiteNonNegative(input.row_gap),
288
+ .column_gap = finiteNonNegative(input.column_gap),
289
+ };
290
+ }
291
+
292
+ fn optionalFinite(value: f32) ?f32 {
293
+ return if (std.math.isFinite(value)) value else null;
294
+ }
295
+
150
296
  /// Resolve an equal-track grid in content coordinates for native hosts.
151
297
  pub export fn craft_native_grid(
152
298
  available_width: f32,
@@ -591,16 +737,20 @@ fn layoutNode(
591
737
  frames[frame_index] = .{ .x = x, .y = y, .width = width, .height = height };
592
738
 
593
739
  if (node.children.len == 0) return frame_index + 1;
594
- const content_width = @max(0, width - node.style.padding_left - node.style.padding_right);
595
- const content_height = @max(0, height - node.style.padding_top - node.style.padding_bottom);
596
- const content_x = x + node.style.padding_left;
597
- const content_y = y + node.style.padding_top;
740
+ const padding_left = finiteNonNegative(node.style.padding_left);
741
+ const padding_right = finiteNonNegative(node.style.padding_right);
742
+ const padding_top = finiteNonNegative(node.style.padding_top);
743
+ const padding_bottom = finiteNonNegative(node.style.padding_bottom);
744
+ const content_width = @max(0, width - padding_left - padding_right);
745
+ const content_height = @max(0, height - padding_top - padding_bottom);
746
+ const content_x = x + padding_left;
747
+ const content_y = y + padding_top;
598
748
  if (node.style.grid) return layoutGrid(node, content_width, content_height, content_x, content_y, frame_index, frames);
599
749
 
600
750
  const main_size = if (node.style.direction == .row) content_width else content_height;
601
- const gap = @max(0, node.style.gap);
751
+ const main_gap = flexAxisGap(node.style, node.style.direction);
602
752
  const gap_count = if (node.children.len > 1) node.children.len - 1 else 0;
603
- const gap_total = gap * @as(f32, @floatFromInt(gap_count));
753
+ const gap_total = main_gap * @as(f32, @floatFromInt(gap_count));
604
754
  if (node.style.flex_wrap) return layoutWrapped(node, content_width, content_height, content_x, content_y, frame_index, frames);
605
755
  const flex = resolveFlex(node.children, node.style.direction, main_size, gap_total);
606
756
  const line_baseline = baselineForChildren(node.children, node.style.direction, content_width, content_height, content_height, node.style.align_items);
@@ -610,7 +760,7 @@ fn layoutNode(
610
760
  }
611
761
  const free_main = @max(0, main_size - used_main);
612
762
  var leading_space: f32 = 0;
613
- var between_space: f32 = gap;
763
+ var between_space: f32 = main_gap;
614
764
  switch (node.style.justify_content) {
615
765
  .center => leading_space = free_main / 2,
616
766
  .flex_end => leading_space = free_main,
@@ -667,21 +817,22 @@ fn layoutWrapped(
667
817
  const direction = node.style.direction;
668
818
  const main_size = if (direction == .row) width else height;
669
819
  const cross_size = if (direction == .row) height else width;
670
- const gap = @max(0, node.style.gap);
820
+ const main_gap = flexAxisGap(node.style, direction);
821
+ const cross_gap = crossAxisGap(node.style, direction);
671
822
  var line_count: usize = 0;
672
823
  var cross_total: f32 = 0;
673
824
  var start: usize = 0;
674
825
  while (start < node.children.len) {
675
- const end = wrappedLineEnd(node.children, start, direction, main_size, gap);
826
+ const end = wrappedLineEnd(node.children, start, direction, main_size, main_gap);
676
827
  cross_total += wrappedLineCross(node.children[start..end], direction);
677
- if (line_count > 0) cross_total += gap;
828
+ if (line_count > 0) cross_total += cross_gap;
678
829
  line_count += 1;
679
830
  start = end;
680
831
  }
681
832
  const free_cross = @max(0, cross_size - cross_total);
682
833
  var line_extra: f32 = 0;
683
834
  var cross_offset: f32 = 0;
684
- var cross_between = gap;
835
+ var cross_between = cross_gap;
685
836
  switch (node.style.align_content) {
686
837
  .center => cross_offset = free_cross / 2,
687
838
  .flex_end => cross_offset = free_cross,
@@ -702,15 +853,15 @@ fn layoutWrapped(
702
853
  var next_index = frame_index + 1;
703
854
  start = 0;
704
855
  while (start < node.children.len) {
705
- const end = wrappedLineEnd(node.children, start, direction, main_size, gap);
856
+ const end = wrappedLineEnd(node.children, start, direction, main_size, main_gap);
706
857
  const children = node.children[start..end];
707
858
  const line_cross = wrappedLineCross(children, direction) + line_extra;
708
- const flex = resolveFlex(children, direction, main_size, gap * @as(f32, @floatFromInt(if (children.len > 1) children.len - 1 else 0)));
709
- var used_main: f32 = gap * @as(f32, @floatFromInt(if (children.len > 1) children.len - 1 else 0));
859
+ const flex = resolveFlex(children, direction, main_size, main_gap * @as(f32, @floatFromInt(if (children.len > 1) children.len - 1 else 0)));
860
+ var used_main: f32 = main_gap * @as(f32, @floatFromInt(if (children.len > 1) children.len - 1 else 0));
710
861
  for (children) |child| used_main += flex.size(child, childMainBasis(child, direction)) + mainMargins(child, direction);
711
862
  const free_main = @max(0, main_size - used_main);
712
863
  var leading_space: f32 = 0;
713
- var between_space: f32 = gap;
864
+ var between_space: f32 = main_gap;
714
865
  distributeJustify(node.style.justify_content, free_main, children.len, &leading_space, &between_space);
715
866
  const line_baseline = baselineForChildren(children, direction, width, height, line_cross, node.style.align_items);
716
867
  var main_offset = leading_space;
@@ -867,8 +1018,8 @@ fn layoutGrid(
867
1018
  ) LayoutError!usize {
868
1019
  const columns = @max(@as(u32, 1), node.style.grid_columns);
869
1020
  const column_count: usize = @intCast(columns);
870
- const column_gap = @max(0, node.style.column_gap);
871
- const row_gap = @max(0, node.style.row_gap);
1021
+ const column_gap = axisGap(node.style.column_gap, node.style.gap);
1022
+ const row_gap = axisGap(node.style.row_gap, node.style.gap);
872
1023
  const available_width = @max(0, width - column_gap * @as(f32, @floatFromInt(column_count - 1)));
873
1024
  const cell_width = available_width / @as(f32, @floatFromInt(column_count));
874
1025
  var next_index = frame_index + 1;
@@ -1073,6 +1224,66 @@ fn finiteNonNegative(value: f32) f32 {
1073
1224
  return if (!std.math.isFinite(value) or value < 0) 0 else value;
1074
1225
  }
1075
1226
 
1227
+ /// `gap` is the shorthand fallback for an axis-specific gap. The flat ABI
1228
+ /// uses zero for an omitted row/column value, so a positive axis value wins
1229
+ /// while an unset value continues to inherit the shorthand.
1230
+ fn axisGap(explicit: f32, shorthand: f32) f32 {
1231
+ const value = finiteNonNegative(explicit);
1232
+ return if (value > 0) value else finiteNonNegative(shorthand);
1233
+ }
1234
+
1235
+ fn flexAxisGap(style: Style, direction: Axis) f32 {
1236
+ return if (direction == .row)
1237
+ axisGap(style.column_gap, style.gap)
1238
+ else
1239
+ axisGap(style.row_gap, style.gap);
1240
+ }
1241
+
1242
+ fn crossAxisGap(style: Style, direction: Axis) f32 {
1243
+ return if (direction == .row)
1244
+ axisGap(style.row_gap, style.gap)
1245
+ else
1246
+ axisGap(style.column_gap, style.gap);
1247
+ }
1248
+
1249
+ fn emptyLayoutStyleInput() LayoutStyleInput {
1250
+ const unset = std.math.nan(f32);
1251
+ return .{
1252
+ .direction = 0,
1253
+ .width = unset,
1254
+ .height = unset,
1255
+ .intrinsic_width = unset,
1256
+ .intrinsic_height = unset,
1257
+ .baseline = unset,
1258
+ .aspect_ratio = unset,
1259
+ .flex_basis = unset,
1260
+ .flex_grow = 0,
1261
+ .flex_shrink = 0,
1262
+ .min_width = unset,
1263
+ .max_width = unset,
1264
+ .min_height = unset,
1265
+ .max_height = unset,
1266
+ .gap = 0,
1267
+ .justify_content = 0,
1268
+ .align_items = 3,
1269
+ .align_self = 0,
1270
+ .flex_wrap = 0,
1271
+ .align_content = 6,
1272
+ .margin_top = 0,
1273
+ .margin_right = 0,
1274
+ .margin_bottom = 0,
1275
+ .margin_left = 0,
1276
+ .padding_top = 0,
1277
+ .padding_right = 0,
1278
+ .padding_bottom = 0,
1279
+ .padding_left = 0,
1280
+ .grid = 0,
1281
+ .grid_columns = 1,
1282
+ .row_gap = 0,
1283
+ .column_gap = 0,
1284
+ };
1285
+ }
1286
+
1076
1287
  fn clampBounds(value: f32, lower: f32, upper: f32) f32 {
1077
1288
  var result = finiteNonNegative(value);
1078
1289
  if (std.math.isFinite(lower) and lower >= 0) result = @max(result, lower);
@@ -1088,6 +1299,92 @@ fn optionalDimension(explicit: f32, intrinsic: f32) f32 {
1088
1299
  return finiteNonNegative(if (isUnset(explicit)) intrinsic else explicit);
1089
1300
  }
1090
1301
 
1302
+ test "exports the host-neutral tree layout contract" {
1303
+ var root_style = emptyLayoutStyleInput();
1304
+ root_style.width = 100;
1305
+ root_style.height = 40;
1306
+ root_style.padding_left = 8;
1307
+ root_style.padding_top = 4;
1308
+ root_style.gap = 5;
1309
+ root_style.justify_content = 1;
1310
+
1311
+ var child_style = emptyLayoutStyleInput();
1312
+ child_style.intrinsic_width = 20;
1313
+ child_style.intrinsic_height = 10;
1314
+
1315
+ const nodes = [_]LayoutNodeInput{
1316
+ .{ .style = root_style, .first_child = 1, .child_count = 1 },
1317
+ .{ .style = child_style, .first_child = 0, .child_count = 0 },
1318
+ };
1319
+ var frames: [2]Frame = undefined;
1320
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_layout_tree(100, 40, &nodes, nodes.len, &frames, frames.len));
1321
+ try std.testing.expectEqual(@as(f32, 100), frames[0].width);
1322
+ try std.testing.expectEqual(@as(f32, 40), frames[0].height);
1323
+ try std.testing.expectEqual(@as(f32, 44), frames[1].x);
1324
+ try std.testing.expectEqual(@as(f32, 4), frames[1].y);
1325
+ try std.testing.expectEqual(@as(f32, 20), frames[1].width);
1326
+ try std.testing.expectEqual(@as(f32, 10), frames[1].height);
1327
+ }
1328
+
1329
+ test "lays out nested flat trees with absolute descendant frames" {
1330
+ var root_style = emptyLayoutStyleInput();
1331
+ root_style.width = 200;
1332
+ root_style.height = 100;
1333
+ root_style.padding_left = 10;
1334
+ root_style.padding_top = 5;
1335
+ root_style.gap = 10;
1336
+
1337
+ var column_style = emptyLayoutStyleInput();
1338
+ column_style.direction = 1;
1339
+ column_style.width = 80;
1340
+ column_style.height = 60;
1341
+ column_style.padding_left = 4;
1342
+ column_style.padding_top = 3;
1343
+ column_style.gap = 2;
1344
+
1345
+ var first_style = emptyLayoutStyleInput();
1346
+ first_style.intrinsic_width = 20;
1347
+ first_style.intrinsic_height = 10;
1348
+ var second_style = emptyLayoutStyleInput();
1349
+ second_style.intrinsic_width = 30;
1350
+ second_style.intrinsic_height = 15;
1351
+
1352
+ const nodes = [_]LayoutNodeInput{
1353
+ .{ .style = root_style, .first_child = 1, .child_count = 1 },
1354
+ .{ .style = column_style, .first_child = 2, .child_count = 2 },
1355
+ .{ .style = first_style, .first_child = 0, .child_count = 0 },
1356
+ .{ .style = second_style, .first_child = 0, .child_count = 0 },
1357
+ };
1358
+ var frames: [4]Frame = undefined;
1359
+ try std.testing.expectEqual(@as(c_int, 0), craft_native_layout_tree(200, 100, &nodes, nodes.len, &frames, frames.len));
1360
+ try std.testing.expectEqual(@as(f32, 200), frames[0].width);
1361
+ try std.testing.expectEqual(@as(f32, 100), frames[0].height);
1362
+ try std.testing.expectEqual(@as(f32, 10), frames[1].x);
1363
+ try std.testing.expectEqual(@as(f32, 5), frames[1].y);
1364
+ try std.testing.expectEqual(@as(f32, 80), frames[1].width);
1365
+ try std.testing.expectEqual(@as(f32, 60), frames[1].height);
1366
+ try std.testing.expectEqual(@as(f32, 14), frames[2].x);
1367
+ try std.testing.expectEqual(@as(f32, 8), frames[2].y);
1368
+ try std.testing.expectEqual(@as(f32, 20), frames[2].width);
1369
+ try std.testing.expectEqual(@as(f32, 10), frames[2].height);
1370
+ try std.testing.expectEqual(@as(f32, 14), frames[3].x);
1371
+ try std.testing.expectEqual(@as(f32, 20), frames[3].y);
1372
+ try std.testing.expectEqual(@as(f32, 30), frames[3].width);
1373
+ try std.testing.expectEqual(@as(f32, 15), frames[3].height);
1374
+ }
1375
+
1376
+ test "rejects malformed host-neutral layout trees without writing frames" {
1377
+ var style = emptyLayoutStyleInput();
1378
+ style.width = 20;
1379
+ style.height = 20;
1380
+ const nodes = [_]LayoutNodeInput{
1381
+ .{ .style = style, .first_child = 0, .child_count = 1 },
1382
+ };
1383
+ var frames = [_]Frame{.{ .x = 7, .y = 8, .width = 9, .height = 10 }};
1384
+ try std.testing.expectEqual(@as(c_int, -1), craft_native_layout_tree(20, 20, &nodes, nodes.len, &frames, frames.len));
1385
+ try std.testing.expectEqual(@as(f32, 7), frames[0].x);
1386
+ }
1387
+
1091
1388
  test "distributes positive flex grow from a shared basis" {
1092
1389
  const children = [_]Node{
1093
1390
  .{ .style = .{ .flex_basis = 40, .flex_grow = 1 } },
@@ -1219,6 +1516,32 @@ test "aligns wrapped children using align self" {
1219
1516
  try std.testing.expectApproxEqAbs(@as(f32, 24), frames[3].y, 0.001);
1220
1517
  }
1221
1518
 
1519
+ test "keeps independent row and column gaps in wrapped flex" {
1520
+ const children = [_]Node{
1521
+ .{ .style = .{ .width = 45, .height = 10 } },
1522
+ .{ .style = .{ .width = 45, .height = 10 } },
1523
+ .{ .style = .{ .width = 45, .height = 10 } },
1524
+ };
1525
+ const root = Node{
1526
+ .style = .{
1527
+ .direction = .row,
1528
+ .width = 100,
1529
+ .height = 80,
1530
+ .flex_wrap = true,
1531
+ .column_gap = 5,
1532
+ .row_gap = 12,
1533
+ .align_items = .flex_start,
1534
+ .align_content = .flex_start,
1535
+ },
1536
+ .children = &children,
1537
+ };
1538
+ var frames: [4]Frame = undefined;
1539
+
1540
+ _ = try layout(root, .{ .width = 100, .height = 80 }, &frames);
1541
+ try std.testing.expectApproxEqAbs(@as(f32, 50), frames[2].x, 0.001);
1542
+ try std.testing.expectApproxEqAbs(@as(f32, 22), frames[3].y, 0.001);
1543
+ }
1544
+
1222
1545
  test "uses viewport size when the root is intrinsic" {
1223
1546
  var frames: [1]Frame = undefined;
1224
1547
  const count = try layout(.{}, .{ .width = 320, .height = 640 }, &frames);
@@ -1309,6 +1632,55 @@ test "sanitizes non-finite tree dimensions" {
1309
1632
  }
1310
1633
  }
1311
1634
 
1635
+ test "sanitizes non-finite and negative tree padding" {
1636
+ const children = [_]Node{.{ .style = .{ .flex_basis = 10 } }};
1637
+ const root = Node{
1638
+ .style = .{
1639
+ .width = 100,
1640
+ .height = 40,
1641
+ .padding_left = -8,
1642
+ .padding_right = std.math.nan(f32),
1643
+ .padding_top = std.math.inf(f32),
1644
+ .padding_bottom = -4,
1645
+ },
1646
+ .children = &children,
1647
+ };
1648
+ var frames: [2]Frame = undefined;
1649
+
1650
+ _ = try layout(root, .{ .width = 100, .height = 40 }, &frames);
1651
+ try std.testing.expectApproxEqAbs(@as(f32, 0), frames[1].x, 0.001);
1652
+ try std.testing.expectApproxEqAbs(@as(f32, 0), frames[1].y, 0.001);
1653
+ }
1654
+
1655
+ test "sanitizes non-finite tree flex and grid gaps" {
1656
+ const flex_children = [_]Node{
1657
+ .{ .style = .{ .flex_basis = 10 } },
1658
+ .{ .style = .{ .flex_basis = 10 } },
1659
+ };
1660
+ const flex_root = Node{
1661
+ .style = .{ .direction = .row, .width = 40, .height = 20, .gap = std.math.nan(f32) },
1662
+ .children = &flex_children,
1663
+ };
1664
+ var flex_frames: [3]Frame = undefined;
1665
+ _ = try layout(flex_root, .{ .width = 40, .height = 20 }, &flex_frames);
1666
+ try std.testing.expectApproxEqAbs(@as(f32, 10), flex_frames[2].x, 0.001);
1667
+
1668
+ const grid_children = [_]Node{
1669
+ .{ .style = .{ .width = 10, .height = 10 } },
1670
+ .{ .style = .{ .width = 10, .height = 10 } },
1671
+ };
1672
+ const grid_root = Node{
1673
+ .style = .{ .grid = true, .grid_columns = 2, .width = 40, .height = 20, .column_gap = std.math.inf(f32), .row_gap = -4 },
1674
+ .children = &grid_children,
1675
+ };
1676
+ var grid_frames: [3]Frame = undefined;
1677
+ _ = try layout(grid_root, .{ .width = 40, .height = 20 }, &grid_frames);
1678
+ for (grid_frames) |frame| {
1679
+ try std.testing.expect(std.math.isFinite(frame.x));
1680
+ try std.testing.expect(std.math.isFinite(frame.y));
1681
+ }
1682
+ }
1683
+
1312
1684
  test "recurses across row and column axes without host concepts" {
1313
1685
  const grandchildren = [_]Node{
1314
1686
  .{ .style = .{ .flex_basis = 10, .flex_grow = 1 } },
package/dist/mobile.js CHANGED
@@ -713,6 +713,71 @@ var speech = {
713
713
  getWebSpeech()?.synthesis.cancel();
714
714
  }
715
715
  };
716
+ function crashReporterBridge() {
717
+ const root = globalThis.craft;
718
+ return root?.crashReporter && typeof root.crashReporter.capture === "function" ? root.crashReporter : undefined;
719
+ }
720
+ var crashReporter = {
721
+ isAvailable() {
722
+ const root = globalThis.craft;
723
+ if (!crashReporterBridge())
724
+ return false;
725
+ const caps = root?.capabilities;
726
+ if (Array.isArray(caps))
727
+ return caps.includes("crashReporter");
728
+ return caps?.crashReporter !== false;
729
+ },
730
+ async capture(event) {
731
+ const bridge = crashReporterBridge();
732
+ if (!bridge || !event || typeof event.message !== "string")
733
+ return false;
734
+ try {
735
+ return await bridge.capture(event) !== false;
736
+ } catch {
737
+ return false;
738
+ }
739
+ },
740
+ async setUser(id) {
741
+ const bridge = crashReporterBridge();
742
+ if (!bridge)
743
+ return false;
744
+ try {
745
+ return await bridge.setUser(id == null ? null : String(id)) !== false;
746
+ } catch {
747
+ return false;
748
+ }
749
+ },
750
+ async addBreadcrumb(breadcrumb) {
751
+ const bridge = crashReporterBridge();
752
+ if (!bridge)
753
+ return false;
754
+ try {
755
+ return await bridge.addBreadcrumb(breadcrumb) !== false;
756
+ } catch {
757
+ return false;
758
+ }
759
+ },
760
+ async flush() {
761
+ const bridge = crashReporterBridge();
762
+ if (!bridge)
763
+ return false;
764
+ try {
765
+ return await bridge.flush() !== false;
766
+ } catch {
767
+ return false;
768
+ }
769
+ },
770
+ async context() {
771
+ const bridge = crashReporterBridge();
772
+ if (!bridge)
773
+ return null;
774
+ try {
775
+ return await bridge.context();
776
+ } catch {
777
+ return null;
778
+ }
779
+ }
780
+ };
716
781
  var MUSIC_AUTHORIZATIONS = ["authorized", "denied", "restricted", "notDetermined", "unavailable"];
717
782
  var MUSIC_STATES = ["stopped", "playing", "paused", "interrupted", "seekingForward", "seekingBackward", "unknown"];
718
783
  function musicAuthorization(value) {
@@ -1607,6 +1672,7 @@ export {
1607
1672
  dialog,
1608
1673
  device,
1609
1674
  deepLinks,
1675
+ crashReporter,
1610
1676
  contextMenu,
1611
1677
  chrome,
1612
1678
  camera,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.175",
3
+ "version": "0.0.177",
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>",