craft-native 0.0.160 → 0.0.162

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.
@@ -298,6 +298,43 @@ object CraftNative {
298
298
  margins: FloatArray,
299
299
  flags: FloatArray,
300
300
  ): FloatArray?
301
+ private external fun nativeGridAligned(
302
+ availableWidth: Float,
303
+ availableHeight: Float,
304
+ columns: Int,
305
+ columnGap: Float,
306
+ rowGap: Float,
307
+ alignContent: Int,
308
+ intrinsicWidth: FloatArray,
309
+ intrinsicHeight: FloatArray,
310
+ explicitWidth: FloatArray,
311
+ explicitHeight: FloatArray,
312
+ minWidth: FloatArray,
313
+ maxWidth: FloatArray,
314
+ minHeight: FloatArray,
315
+ maxHeight: FloatArray,
316
+ margins: FloatArray,
317
+ flags: FloatArray,
318
+ ): FloatArray?
319
+ private external fun nativeGridAutoRowsAligned(
320
+ availableWidth: Float,
321
+ availableHeight: Float,
322
+ columns: Int,
323
+ columnGap: Float,
324
+ rowGap: Float,
325
+ alignContent: Int,
326
+ autoRowHeight: Float,
327
+ intrinsicWidth: FloatArray,
328
+ intrinsicHeight: FloatArray,
329
+ explicitWidth: FloatArray,
330
+ explicitHeight: FloatArray,
331
+ minWidth: FloatArray,
332
+ maxWidth: FloatArray,
333
+ minHeight: FloatArray,
334
+ maxHeight: FloatArray,
335
+ margins: FloatArray,
336
+ flags: FloatArray,
337
+ ): FloatArray?
301
338
  private external fun nativeClipboardRead(activity: Activity): String?
302
339
  private external fun nativeClipboardWrite(activity: Activity, text: String): Boolean
303
340
  private external fun nativeOpenUrl(activity: Activity, url: String): Boolean
@@ -532,13 +569,14 @@ object CraftNative {
532
569
  margins: FloatArray,
533
570
  flags: FloatArray,
534
571
  autoRowHeight: Float? = null,
572
+ alignContent: Int = 0,
535
573
  ): FloatArray? {
536
574
  if (!isAvailable) return null
537
575
  return try {
538
576
  if (autoRowHeight != null) {
539
- nativeGridAutoRows(availableWidth, availableHeight, columns, columnGap, rowGap, autoRowHeight, intrinsicWidth, intrinsicHeight, explicitWidth, explicitHeight, minWidth, maxWidth, minHeight, maxHeight, margins, flags)
577
+ nativeGridAutoRowsAligned(availableWidth, availableHeight, columns, columnGap, rowGap, alignContent, autoRowHeight, intrinsicWidth, intrinsicHeight, explicitWidth, explicitHeight, minWidth, maxWidth, minHeight, maxHeight, margins, flags)
540
578
  } else {
541
- nativeGrid(availableWidth, availableHeight, columns, columnGap, rowGap, intrinsicWidth, intrinsicHeight, explicitWidth, explicitHeight, minWidth, maxWidth, minHeight, maxHeight, margins, flags)
579
+ nativeGridAligned(availableWidth, availableHeight, columns, columnGap, rowGap, alignContent, intrinsicWidth, intrinsicHeight, explicitWidth, explicitHeight, minWidth, maxWidth, minHeight, maxHeight, margins, flags)
542
580
  }
543
581
  ?.takeIf { it.size >= intrinsicWidth.size * 4 && it.all { value -> value.isFinite() } }
544
582
  } catch (e: UnsatisfiedLinkError) {
@@ -303,7 +303,8 @@ internal class CraftNativeCapabilities(
303
303
  return FileResult.Multipart(body.toByteArray(), boundary)
304
304
  }
305
305
 
306
- private fun files(requestToken: String, method: String, args: JSONArray): CraftNativeCapabilityResult = when (method) {
306
+ private fun files(requestToken: String, method: String, args: JSONArray): CraftNativeCapabilityResult {
307
+ return when (method) {
307
308
  "write" -> {
308
309
  val name = args.optString(0).trim()
309
310
  if (name.isBlank()) return failure("INVALID_ARGUMENT", "files.write needs a file name")
@@ -322,7 +323,8 @@ internal class CraftNativeCapabilities(
322
323
  success(file.delete())
323
324
  }
324
325
  "download" -> download(requestToken, args)
325
- else -> failure("UNKNOWN_ACTION", "Unsupported Files method")
326
+ else -> failure("UNKNOWN_ACTION", "Unsupported Files method")
327
+ }
326
328
  }
327
329
 
328
330
  private fun download(requestToken: String, args: JSONArray): CraftNativeCapabilityResult {
@@ -115,6 +115,7 @@ private data class CraftNativeLength(
115
115
  private data class CraftNativeLayoutStyle(
116
116
  val width: CraftNativeLength? = null,
117
117
  val height: CraftNativeLength? = null,
118
+ val aspectRatio: Double? = null,
118
119
  val minWidth: CraftNativeLength? = null,
119
120
  val maxWidth: CraftNativeLength? = null,
120
121
  val minHeight: CraftNativeLength? = null,
@@ -143,6 +144,7 @@ private data class CraftNativeLayoutStyle(
143
144
  val flex = decimal("flex")
144
145
  return CraftNativeLayoutStyle(
145
146
  width = length("width"), height = length("height"),
147
+ aspectRatio = decimal("aspectRatio")?.takeIf { it.isFinite() && it > 0.0 },
146
148
  minWidth = length("minWidth"), maxWidth = length("maxWidth"),
147
149
  minHeight = length("minHeight"), maxHeight = length("maxHeight"),
148
150
  position = raw.opt("position")
@@ -583,7 +585,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
583
585
  // The STX style is authoritative. A reused View may still carry a
584
586
  // previous fixed LayoutParams width after a reactive width is removed;
585
587
  // using that value here prevents intrinsic sizing from being restored.
586
- val explicit = style.width?.resolve(base) ?: flexBasis
588
+ val authored = style.width?.resolve(base)
589
+ val explicit = authored ?: style.aspectRatio?.let { ratio -> style.height?.resolve(base)?.times(ratio)?.roundToInt() } ?: flexBasis
587
590
  if (explicit != null) {
588
591
  child.measure(
589
592
  View.MeasureSpec.makeMeasureSpec(explicit, View.MeasureSpec.EXACTLY),
@@ -613,7 +616,8 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
613
616
  val flexBasis = if (orientation == VERTICAL) style.flexBasis?.resolve(base) else null
614
617
  // Match width measurement: do not let stale native LayoutParams keep
615
618
  // a removed reactive height fixed.
616
- val explicit = style.height?.resolve(base) ?: flexBasis
619
+ val authored = style.height?.resolve(base)
620
+ val explicit = authored ?: style.aspectRatio?.let { ratio -> style.width?.resolve(base)?.div(ratio)?.roundToInt() } ?: flexBasis
617
621
  val target = explicit ?: base
618
622
  val spec = if (explicit != null) View.MeasureSpec.makeMeasureSpec(target, View.MeasureSpec.EXACTLY)
619
623
  else View.MeasureSpec.makeMeasureSpec(base, View.MeasureSpec.AT_MOST)
@@ -649,6 +653,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
649
653
  "space-between" -> 3
650
654
  "space-around" -> 4
651
655
  "stretch" -> 5
656
+ "space-evenly" -> 6
652
657
  else -> 0
653
658
  }
654
659
 
@@ -772,6 +777,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
772
777
  listOf(if (align == "stretch") 1f else 0f, if (align == "center") 1f else if (align == "flex-end") 2f else 0f)
773
778
  }.toFloatArray(),
774
779
  gridAutoRows?.resolve(contentHeight)?.toFloat(),
780
+ alignContentCode(),
775
781
  )
776
782
  if (sharedFrames != null) {
777
783
  for ((index, entry) in flow.withIndex()) {
@@ -783,9 +789,30 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
783
789
  entry.view.layout(x, y, x + childWidth, y + childHeight)
784
790
  }
785
791
  } else {
786
- var rowTop = paddingTop
792
+ val totalRows = layout.rowHeights.sum()
793
+ val rowGaps = max(0, layout.rowHeights.size - 1) * rowGap
794
+ val freeRows = max(0, contentHeight - totalRows - rowGaps)
795
+ val rowCount = max(1, layout.rowHeights.size)
796
+ var rowLeading = 0
797
+ var rowBetween = rowGap
798
+ var rowExtra = 0
799
+ when (alignContentCode()) {
800
+ 1 -> rowLeading = freeRows / 2
801
+ 2 -> rowLeading = freeRows
802
+ 3 -> if (rowCount > 1) rowBetween += freeRows / (rowCount - 1)
803
+ 4 -> {
804
+ rowLeading = freeRows / (rowCount * 2)
805
+ rowBetween += freeRows / rowCount
806
+ }
807
+ 6 -> {
808
+ rowLeading = freeRows / (rowCount + 1)
809
+ rowBetween += freeRows / (rowCount + 1)
810
+ }
811
+ 5 -> rowExtra = freeRows / rowCount
812
+ }
813
+ var rowTop = paddingTop + rowLeading
787
814
  for ((rowIndex, row) in layout.rows.withIndex()) {
788
- val rowHeight = layout.rowHeights[rowIndex]
815
+ val rowHeight = layout.rowHeights[rowIndex] + rowExtra
789
816
  for ((column, entry) in row.withIndex()) {
790
817
  val align = entry.style.alignSelf ?: alignItems
791
818
  val lp = entry.view.layoutParams as? MarginLayoutParams
@@ -809,7 +836,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
809
836
  }
810
837
  entry.view.layout(x, y, x + childWidth, y + childHeight)
811
838
  }
812
- rowTop += rowHeight + rowGap
839
+ rowTop += rowHeight + rowBetween
813
840
  }
814
841
  }
815
842
  for (entry in layout.absolute) {
@@ -849,6 +876,10 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
849
876
  crossBetween = crossFree / lines.size + crossGap
850
877
  crossLeading = crossFree / (lines.size * 2)
851
878
  }
879
+ "space-evenly" -> {
880
+ crossBetween = crossFree / (lines.size + 1) + crossGap
881
+ crossLeading = crossFree / (lines.size + 1)
882
+ }
852
883
  "stretch" -> {
853
884
  val extra = crossFree / lines.size
854
885
  lines.forEach { it.cross += extra }
@@ -2803,7 +2834,7 @@ class MainActivity : AppCompatActivity() {
2803
2834
  "Svg" -> (previous as? CraftNativeSvgView ?: CraftNativeSvgView(this)).apply {
2804
2835
  drawing = node
2805
2836
  }
2806
- "ScrollView" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
2837
+ "ScrollView", "SectionList" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
2807
2838
  val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection") in setOf("row", "row-reverse")
2808
2839
  setHorizontal(horizontal)
2809
2840
  setNativeNestedScrollingEnabled(true)
@@ -3064,6 +3095,7 @@ class MainActivity : AppCompatActivity() {
3064
3095
  "video" -> "Video"
3065
3096
  "icon" -> "Icon"
3066
3097
  "scrollview" -> "ScrollView"
3098
+ "sectionlist" -> "SectionList"
3067
3099
  "flatlist" -> "FlatList"
3068
3100
  "svg" -> "Svg"
3069
3101
  "touchableopacity" -> "TouchableOpacity"
@@ -3477,7 +3509,9 @@ class MainActivity : AppCompatActivity() {
3477
3509
  else -> null
3478
3510
  }
3479
3511
  view.tooltipText = props.optString("accessibilityHint").takeIf { it.isNotBlank() }
3480
- val role = props.optString("accessibilityRole").ifBlank { type.lowercase() }
3512
+ val role = props.optString("accessibilityRole").ifBlank {
3513
+ if (type == "Icon") "image" else type.lowercase()
3514
+ }
3481
3515
  val state = props.optJSONObject("accessibilityState")
3482
3516
  val disabled = props.optBoolean("disabled", false) || state?.optBoolean("disabled", false) == true
3483
3517
  if (type !in setOf("Button", "Link", "Checkbox", "TextInput", "Picker", "DatePicker", "Modal", "Switch", "Slider", "ActivityIndicator")) {
@@ -1240,9 +1240,11 @@ export declare const pushNotifications: {
1240
1240
  };
1241
1241
  export type HealthDataType = 'steps' | 'heartRate' | 'activeEnergy' | 'distance' | 'workouts' | 'restingHeartRate' | 'heartRateVariability' | 'bodyMass' | 'sleep'
1242
1242
  /** Food logged by any app, per day: kcal eaten, then grams of protein, carbohydrate and fat. Read only. */
1243
- | 'dietaryEnergy' | 'dietaryProtein' | 'dietaryCarbohydrates' | 'dietaryFat';
1243
+ | 'dietaryEnergy' | 'dietaryProtein' | 'dietaryCarbohydrates' | 'dietaryFat'
1244
+ /** Mindful minutes (meditation, breathing): read with getMindfulSessions, written with saveMindfulSession. */
1245
+ | 'mindful';
1244
1246
  /** The types with one value per day. `sleep` is hours asleep, counted on the day you woke. */
1245
- export type HealthDailyType = Exclude<HealthDataType, 'workouts'>;
1247
+ export type HealthDailyType = Exclude<HealthDataType, 'workouts' | 'mindful'>;
1246
1248
  export interface HealthAuthorizationOptions {
1247
1249
  /** `false` asks to read only. Default: also allowed to save workouts. */
1248
1250
  write?: boolean;
@@ -1350,8 +1352,49 @@ export interface HealthSleepInterval {
1350
1352
  sourceName: string;
1351
1353
  deviceName?: string;
1352
1354
  }
1353
- /** The health reads a host may or may not have, depending on the app build it runs in. */
1354
- export type HealthReadMethod = 'getWorkouts' | 'getDailyStatistics' | 'getHeartRateSamples' | 'getSleepIntervals';
1355
+ /** A meditation or breathing session to record as mindful minutes. */
1356
+ export interface HealthMindfulSession {
1357
+ /**
1358
+ * The app's own id for the session (up to 128 characters), kept as the
1359
+ * sample's external UUID. Saving the same id again keeps the first sample.
1360
+ */
1361
+ id: string;
1362
+ /** Epoch milliseconds. */
1363
+ startDate: number;
1364
+ /** Epoch milliseconds, after `startDate` and at most a day later. */
1365
+ endDate: number;
1366
+ }
1367
+ export interface HealthMindfulSessionResult {
1368
+ /** HealthKit's UUID for the sample. */
1369
+ id: string;
1370
+ /** The id it was saved with. */
1371
+ externalId: string;
1372
+ /** True when this id was saved before and that sample was kept. */
1373
+ existing: boolean;
1374
+ }
1375
+ export interface HealthMindfulSessionQuery extends HealthSampleQuery {
1376
+ /** At most this many, oldest first. Defaults to 1000, capped at 5000. Start defaults to 30 days ago. */
1377
+ limit?: number;
1378
+ }
1379
+ /** A mindful session in Apple Health, from any app that writes them. */
1380
+ export interface HealthMindfulSample {
1381
+ /** HealthKit's UUID. */
1382
+ id: string;
1383
+ /** Epoch milliseconds. */
1384
+ startDate: number;
1385
+ /** Epoch milliseconds. */
1386
+ endDate: number;
1387
+ durationSeconds: number;
1388
+ sourceName: string;
1389
+ sourceBundleId: string;
1390
+ /** True when this app wrote it. */
1391
+ ownApp: boolean;
1392
+ /** The id the writing app saved it with, when it gave one. */
1393
+ externalId?: string;
1394
+ deviceName?: string;
1395
+ }
1396
+ /** The health reads and writes a host may or may not have, depending on the app build it runs in. */
1397
+ export type HealthReadMethod = 'getWorkouts' | 'getDailyStatistics' | 'getHeartRateSamples' | 'getSleepIntervals' | 'saveMindfulSession' | 'getMindfulSessions';
1355
1398
  export declare const health: {
1356
1399
  /**
1357
1400
  * Whether this app build answers `method`. An app installed before a read
@@ -1374,6 +1417,14 @@ export declare const health: {
1374
1417
  getHeartRateSamples(options?: HealthHeartRateQuery): Promise<HealthHeartRateSample[]>;
1375
1418
  /** Sleep between two instants, oldest first: time in bed and each asleep or awake stage. iOS only for now. */
1376
1419
  getSleepIntervals(options?: HealthSampleQuery): Promise<HealthSleepInterval[]>;
1420
+ /**
1421
+ * Record a meditation or breathing session as mindful minutes. Needs the
1422
+ * `mindful` grant with writing allowed. Saving the same `id` again keeps
1423
+ * the first sample. iOS only for now.
1424
+ */
1425
+ saveMindfulSession(session: HealthMindfulSession): Promise<HealthMindfulSessionResult>;
1426
+ /** Mindful sessions between two instants, oldest first, each marked `ownApp` when this app wrote it. iOS only for now. */
1427
+ getMindfulSessions(options?: HealthMindfulSessionQuery): Promise<HealthMindfulSample[]>;
1377
1428
  };
1378
1429
  export interface LiveActivityState {
1379
1430
  status?: string;
package/dist/cli.js CHANGED
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3303
3303
  `);
3304
3304
  }
3305
3305
  // package.json
3306
- var version = "0.0.160";
3306
+ var version = "0.0.162";
3307
3307
 
3308
3308
  // bin/cli.ts
3309
3309
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -45,7 +45,7 @@ export interface FlexStyle {
45
45
  /** Align self (override parent alignItems) */
46
46
  alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
47
47
  /** Align content (multi-line) */
48
- alignContent?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'space-between' | 'space-around';
48
+ alignContent?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'space-between' | 'space-around' | 'space-evenly';
49
49
  /** Gap between items */
50
50
  gap?: number;
51
51
  /** Row gap */
package/dist/index.cjs CHANGED
@@ -310,7 +310,7 @@ function craftBinaryNotFoundMessage(triedPath) {
310
310
  `);
311
311
  }
312
312
  // package.json
313
- var version = "0.0.160";
313
+ var version = "0.0.162";
314
314
 
315
315
  // src/sdk-version.ts
316
316
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ function craftBinaryNotFoundMessage(triedPath) {
58
58
  `);
59
59
  }
60
60
  // package.json
61
- var version = "0.0.160";
61
+ var version = "0.0.162";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -79,6 +79,15 @@ export interface CraftConfig {
79
79
  * time the page scans or connects.
80
80
  */
81
81
  enableHeartRate?: boolean;
82
+ /**
83
+ * Keep playing sound with the screen locked or the app in the background:
84
+ * a sleep soundscape, a meditation timer's ambience. Adds the `audio`
85
+ * background mode. iOS keeps the app running only while it plays, and App
86
+ * Review expects audible content, so turn it on only for an app that plays
87
+ * some (Web Audio on native screens and in the web view, which use the
88
+ * playback session).
89
+ */
90
+ enableBackgroundAudio?: boolean;
82
91
  /**
83
92
  * Purpose strings in the app's own words, by Info.plist key
84
93
  * (`{ NSAppleMusicUsageDescription: 'Shows the song playing during a workout.' }`).
@@ -490,6 +490,8 @@ function renderBackgroundModes(config) {
490
490
  modes.add("remote-notification");
491
491
  if (config.enableHeartRate)
492
492
  modes.add("bluetooth-central");
493
+ if (config.enableBackgroundAudio)
494
+ modes.add("audio");
493
495
  if (!modes.size)
494
496
  return "";
495
497
  return ` <key>UIBackgroundModes</key>
@@ -2548,6 +2548,22 @@ struct CraftWebView: UIViewRepresentable {
2548
2548
  } else {
2549
2549
  rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
2550
2550
  }
2551
+ case "saveHealthMindfulSession":
2552
+ if config.enableHealthKit {
2553
+ CraftHealth.shared.saveMindfulSession(body: body) { [weak self] in self?.answer(callbackId, $0) }
2554
+ } else {
2555
+ rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
2556
+ }
2557
+ case "getHealthMindfulSessions":
2558
+ if config.enableHealthKit {
2559
+ CraftHealth.shared.getMindfulSessions(
2560
+ startDate: body["startDate"] as? Double,
2561
+ endDate: body["endDate"] as? Double,
2562
+ limit: body["limit"] as? Int
2563
+ ) { [weak self] in self?.answer(callbackId, $0) }
2564
+ } else {
2565
+ rejectCallback(callbackId, error: "HealthKit is disabled", code: "CAPABILITY_DISABLED")
2566
+ }
2551
2567
 
2552
2568
  // MARK: - Live Activities
2553
2569
  case "startLiveActivity":
@@ -5056,6 +5072,14 @@ struct CraftWebView: UIViewRepresentable {
5056
5072
  getSleepIntervals: function(options) {
5057
5073
  options = options || {};
5058
5074
  return craft._invoke('getHealthSleepIntervals', {startDate: options.startDate, endDate: options.endDate});
5075
+ },
5076
+ saveMindfulSession: function(session) {
5077
+ session = session || {};
5078
+ return craft._invoke('saveHealthMindfulSession', {id: session.id, startDate: session.startDate, endDate: session.endDate});
5079
+ },
5080
+ getMindfulSessions: function(options) {
5081
+ options = options || {};
5082
+ return craft._invoke('getHealthMindfulSessions', {startDate: options.startDate, endDate: options.endDate, limit: options.limit});
5059
5083
  }
5060
5084
  };
5061
5085
  // The Music app's song and transport. Nothing is read until
@@ -43,6 +43,10 @@ final class CraftHealth {
43
43
  getHeartRateSamples(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, limit: options(0)["limit"] as? Int, completion: completion)
44
44
  case "getSleepIntervals":
45
45
  getSleepIntervals(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, completion: completion)
46
+ case "saveMindfulSession":
47
+ saveMindfulSession(body: options(0), completion: completion)
48
+ case "getMindfulSessions":
49
+ getMindfulSessions(startDate: options(0)["startDate"] as? Double, endDate: options(0)["endDate"] as? Double, limit: options(0)["limit"] as? Int, completion: completion)
46
50
  default:
47
51
  completion(.failure(CraftHealthError(message: "Unsupported Health method", code: "UNKNOWN_ACTION")))
48
52
  }
@@ -106,6 +110,14 @@ final class CraftHealth {
106
110
  if let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) {
107
111
  readTypes.insert(sleepType)
108
112
  }
113
+ case "mindful":
114
+ // Mindful minutes: a meditation or breathing session. Read
115
+ // to show them, and (unless read only) written by an app
116
+ // that times them.
117
+ if let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) {
118
+ readTypes.insert(mindfulType)
119
+ shareTypes.insert(mindfulType)
120
+ }
109
121
  case "dietaryEnergy", "dietaryProtein", "dietaryCarbohydrates", "dietaryFat":
110
122
  // What a food log (MyFitnessPal, Lose It!, Cronometer...)
111
123
  // writes to Apple Health: read only, never written back.
@@ -559,4 +571,104 @@ final class CraftHealth {
559
571
  healthStore.execute(query)
560
572
  }
561
573
 
574
+ /// Saves a mindful session (Mindful Minutes in the Health app): a
575
+ /// meditation or breathing session that ran from `startDate` to
576
+ /// `endDate`, epoch milliseconds. `id` is the app's own id for it, kept
577
+ /// as the sample's external UUID, so saving the same session twice (a
578
+ /// retry, a second device) keeps the first sample instead of adding
579
+ /// another: the answer then says `existing: true`.
580
+ func saveMindfulSession(body: [String: Any], completion: @escaping Completion) {
581
+ guard let healthStore = healthStore else {
582
+ completion(.failure(CraftHealthError(message: "HealthKit not available")))
583
+ return
584
+ }
585
+ guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
586
+ completion(.failure(CraftHealthError(message: "Mindful sessions are unavailable")))
587
+ return
588
+ }
589
+ guard let id = body["id"] as? String, !id.isEmpty, id.count <= 128,
590
+ let startValue = body["startDate"] as? Double,
591
+ let endValue = body["endDate"] as? Double,
592
+ endValue > startValue,
593
+ endValue - startValue <= 24 * 3600 * 1000 else {
594
+ completion(.failure(CraftHealthError(message: "A valid id, startDate and endDate (at most a day apart) are required", code: "INVALID_ARGUMENT")))
595
+ return
596
+ }
597
+
598
+ let start = Date(timeIntervalSince1970: startValue / 1000)
599
+ let end = Date(timeIntervalSince1970: endValue / 1000)
600
+ // Only this app's own samples can be the same session.
601
+ let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
602
+ HKQuery.predicateForObjects(withMetadataKey: HKMetadataKeyExternalUUID, allowedValues: [id]),
603
+ HKQuery.predicateForObjects(from: HKSource.default()),
604
+ ])
605
+ let lookup = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: 1, sortDescriptors: nil) { _, samples, _ in
606
+ if let kept = samples?.first {
607
+ completion(.success(["id": kept.uuid.uuidString, "externalId": id, "existing": true]))
608
+ return
609
+ }
610
+ let sample = HKCategorySample(
611
+ type: mindfulType,
612
+ value: HKCategoryValue.notApplicable.rawValue,
613
+ start: start,
614
+ end: end,
615
+ metadata: [HKMetadataKeyExternalUUID: id]
616
+ )
617
+ healthStore.save(sample) { success, error in
618
+ if success {
619
+ completion(.success(["id": sample.uuid.uuidString, "externalId": id, "existing": false]))
620
+ } else {
621
+ completion(.failure(CraftHealthError(message: error?.localizedDescription ?? "Mindful session could not be saved")))
622
+ }
623
+ }
624
+ }
625
+ healthStore.execute(lookup)
626
+ }
627
+
628
+ /// Mindful sessions between two instants, oldest first, from every app
629
+ /// that writes them (the Mindfulness app, Calm, this one). Each says
630
+ /// whether this app wrote it (`ownApp`) and carries the external id it
631
+ /// was saved with, so a page can tell its own sessions from the rest.
632
+ func getMindfulSessions(startDate: Double?, endDate: Double?, limit: Int?, completion: @escaping Completion) {
633
+ guard let healthStore = healthStore else {
634
+ completion(.failure(CraftHealthError(message: "HealthKit not available")))
635
+ return
636
+ }
637
+ guard let mindfulType = HKObjectType.categoryType(forIdentifier: .mindfulSession) else {
638
+ completion(.failure(CraftHealthError(message: "Mindful sessions are unavailable")))
639
+ return
640
+ }
641
+
642
+ let end = endDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Date()
643
+ let start = startDate.map { Date(timeIntervalSince1970: $0 / 1000) } ?? Calendar.current.date(byAdding: .day, value: -30, to: end)!
644
+ let predicate = HKQuery.predicateForSamples(withStart: start, end: end, options: [])
645
+ let oldestFirst = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
646
+ let cap = max(1, min(limit ?? 1000, 5000))
647
+ let ownBundle = Bundle.main.bundleIdentifier
648
+
649
+ let query = HKSampleQuery(sampleType: mindfulType, predicate: predicate, limit: cap, sortDescriptors: [oldestFirst]) { _, samples, error in
650
+ if let error = error, (error as? HKError)?.code != .errorNoData {
651
+ completion(.failure(CraftHealthError(message: error.localizedDescription)))
652
+ return
653
+ }
654
+ let rows: [[String: Any]] = (samples ?? []).map { sample in
655
+ let source = sample.sourceRevision.source
656
+ var item: [String: Any] = [
657
+ "id": sample.uuid.uuidString,
658
+ "startDate": sample.startDate.timeIntervalSince1970 * 1000,
659
+ "endDate": sample.endDate.timeIntervalSince1970 * 1000,
660
+ "durationSeconds": sample.endDate.timeIntervalSince(sample.startDate),
661
+ "sourceName": source.name,
662
+ "sourceBundleId": source.bundleIdentifier,
663
+ "ownApp": ownBundle != nil && source.bundleIdentifier == ownBundle,
664
+ ]
665
+ if let external = sample.metadata?[HKMetadataKeyExternalUUID] as? String { item["externalId"] = external }
666
+ if let device = sample.device?.name { item["deviceName"] = device }
667
+ return item
668
+ }
669
+ completion(.success(rows))
670
+ }
671
+ healthStore.execute(query)
672
+ }
673
+
562
674
  }
@@ -185,6 +185,12 @@ private enum CraftNativeGridBridge {
185
185
  private typealias AutoRowsFunction = @convention(c) (
186
186
  Float, Float, UInt32, Float, Float, Float, UnsafeRawPointer, Int, UnsafeMutableRawPointer
187
187
  ) -> Int32
188
+ private typealias AlignedFunction = @convention(c) (
189
+ Float, Float, UInt32, Float, Float, UInt32, UnsafeRawPointer, Int, UnsafeMutableRawPointer
190
+ ) -> Int32
191
+ private typealias AutoRowsAlignedFunction = @convention(c) (
192
+ Float, Float, UInt32, Float, Float, UInt32, Float, UnsafeRawPointer, Int, UnsafeMutableRawPointer
193
+ ) -> Int32
188
194
 
189
195
  private static let function: Function? = {
190
196
  guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_grid") else { return nil }
@@ -194,6 +200,14 @@ private enum CraftNativeGridBridge {
194
200
  guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_grid_auto_rows") else { return nil }
195
201
  return unsafeBitCast(symbol, to: AutoRowsFunction.self)
196
202
  }()
203
+ private static let alignedFunction: AlignedFunction? = {
204
+ guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_grid_aligned") else { return nil }
205
+ return unsafeBitCast(symbol, to: AlignedFunction.self)
206
+ }()
207
+ private static let autoRowsAlignedFunction: AutoRowsAlignedFunction? = {
208
+ guard let symbol = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "craft_native_grid_auto_rows_aligned") else { return nil }
209
+ return unsafeBitCast(symbol, to: AutoRowsAlignedFunction.self)
210
+ }()
197
211
 
198
212
  static func frames(
199
213
  availableWidth: CGFloat,
@@ -202,7 +216,8 @@ private enum CraftNativeGridBridge {
202
216
  columnGap: CGFloat,
203
217
  rowGap: CGFloat,
204
218
  items: [(intrinsicWidth: CGFloat, intrinsicHeight: CGFloat, explicitWidth: CGFloat?, explicitHeight: CGFloat?, minWidth: CGFloat?, maxWidth: CGFloat?, minHeight: CGFloat?, maxHeight: CGFloat?, marginLeft: CGFloat, marginRight: CGFloat, marginTop: CGFloat, marginBottom: CGFloat, stretch: Bool, alignment: UInt32)],
205
- autoRowHeight: CGFloat? = nil
219
+ autoRowHeight: CGFloat? = nil,
220
+ alignContent: UInt32 = 0
206
221
  ) -> [(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat)]? {
207
222
  guard !items.isEmpty else { return nil }
208
223
  if autoRowHeight != nil, autoRowsFunction == nil { return nil }
@@ -222,8 +237,14 @@ private enum CraftNativeGridBridge {
222
237
  let status = input.withUnsafeBufferPointer { inputBuffer in
223
238
  output.withUnsafeMutableBufferPointer { outputBuffer in
224
239
  if let autoRowHeight, let autoRowsFunction {
240
+ if alignContent != 0, let autoRowsAlignedFunction {
241
+ return autoRowsAlignedFunction(Float(availableWidth), Float(availableHeight), UInt32(max(1, columns)), Float(columnGap), Float(rowGap), alignContent, Float(autoRowHeight), UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!))
242
+ }
225
243
  return autoRowsFunction(Float(availableWidth), Float(availableHeight), UInt32(max(1, columns)), Float(columnGap), Float(rowGap), Float(autoRowHeight), UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!))
226
244
  }
245
+ if alignContent != 0, let alignedFunction {
246
+ return alignedFunction(Float(availableWidth), Float(availableHeight), UInt32(max(1, columns)), Float(columnGap), Float(rowGap), alignContent, UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!))
247
+ }
227
248
  guard let function else { return Int32(-1) }
228
249
  return function(Float(availableWidth), Float(availableHeight), UInt32(max(1, columns)), Float(columnGap), Float(rowGap), UnsafeRawPointer(inputBuffer.baseAddress!), input.count, UnsafeMutableRawPointer(outputBuffer.baseAddress!))
229
250
  }
@@ -235,6 +256,18 @@ private enum CraftNativeGridBridge {
235
256
  }
236
257
  }
237
258
 
259
+ private func craftNativeAlignContentCode(_ value: String) -> UInt32 {
260
+ switch value {
261
+ case "center": return 1
262
+ case "flex-end": return 2
263
+ case "space-between": return 3
264
+ case "space-around": return 4
265
+ case "stretch": return 5
266
+ case "space-evenly": return 6
267
+ default: return 0
268
+ }
269
+ }
270
+
238
271
  private final class CraftNativeVideoView: UIView {
239
272
  private let playerController = AVPlayerViewController()
240
273
  private var player: AVPlayer?
@@ -436,6 +469,7 @@ private enum CraftNativeLength: Equatable {
436
469
  private struct CraftNativeLayoutStyle {
437
470
  let width: CraftNativeLength?
438
471
  let height: CraftNativeLength?
472
+ let aspectRatio: CGFloat?
439
473
  let minWidth: CraftNativeLength?
440
474
  let maxWidth: CraftNativeLength?
441
475
  let minHeight: CraftNativeLength?
@@ -465,6 +499,8 @@ private struct CraftNativeLayoutStyle {
465
499
  func number(_ value: Any?) -> CGFloat? { (value as? NSNumber).map { CGFloat(truncating: $0) } }
466
500
  width = CraftNativeLength(raw["width"])
467
501
  height = CraftNativeLength(raw["height"])
502
+ let ratio = number(raw["aspectRatio"])
503
+ aspectRatio = ratio.flatMap { $0.isFinite && $0 > 0 ? $0 : nil }
468
504
  minWidth = CraftNativeLength(raw["minWidth"])
469
505
  maxWidth = CraftNativeLength(raw["maxWidth"])
470
506
  minHeight = CraftNativeLength(raw["minHeight"])
@@ -870,12 +906,13 @@ private class CraftNativeFlowView: UIStackView {
870
906
  let content = CGRect(x: padding.left, y: padding.top, width: max(0, size.width - paddingWidth), height: max(0, size.height - paddingHeight))
871
907
  for child in children where child.craftLayoutStyle.position == "absolute" {
872
908
  let style = child.craftLayoutStyle
873
- let fixedWidth = style.left != nil && style.right != nil
909
+ var fixedWidth = style.left != nil && style.right != nil
874
910
  ? max(0, content.width - (style.left ?? 0) - (style.right ?? 0))
875
911
  : style.width?.resolve(content.width)
876
- let fixedHeight = style.top != nil && style.bottom != nil
912
+ var fixedHeight = style.top != nil && style.bottom != nil
877
913
  ? max(0, content.height - (style.top ?? 0) - (style.bottom ?? 0))
878
914
  : style.height?.resolve(content.height)
915
+ (fixedWidth, fixedHeight) = aspectDimensions(style, width: fixedWidth, height: fixedHeight)
879
916
  let measured = CraftNativeFlexLayout.measure(child, width: fixedWidth, height: fixedHeight, maxWidth: content.width, maxHeight: content.height)
880
917
  let x = style.left ?? (style.right.map { content.width - $0 - measured.width } ?? 0)
881
918
  let y = style.top ?? (style.bottom.map { content.height - $0 - measured.height } ?? 0)
@@ -885,6 +922,21 @@ private class CraftNativeFlowView: UIStackView {
885
922
  return (size, frames)
886
923
  }
887
924
 
925
+ /// Resolves the missing side of an aspect-ratio box after its parent has
926
+ /// supplied any definite width or height. Keeping this at the host edge
927
+ /// lets grid and absolute children follow the same rule as flex items
928
+ /// without adding platform-specific measurement behavior to native views.
929
+ private func aspectDimensions(
930
+ _ style: CraftNativeLayoutStyle,
931
+ width: CGFloat?,
932
+ height: CGFloat?
933
+ ) -> (width: CGFloat?, height: CGFloat?) {
934
+ guard let ratio = style.aspectRatio else { return (width, height) }
935
+ if let width, height == nil { return (width, width / ratio) }
936
+ if let height, width == nil { return (height * ratio, height) }
937
+ return (width, height)
938
+ }
939
+
888
940
  private func layoutFlex(
889
941
  _ children: [UIView],
890
942
  horizontal: Bool,
@@ -964,8 +1016,14 @@ private class CraftNativeFlowView: UIStackView {
964
1016
  let crossLeading = horizontal ? style.marginTop : style.marginLeft
965
1017
  let crossTrailing = horizontal ? style.marginBottom : style.marginRight
966
1018
  let align = style.alignSelf ?? alignItems
967
- let explicitMain = (horizontal ? style.width : style.height)?.resolve(definiteMain)
968
- let explicitCross = (horizontal ? style.height : style.width)?.resolve(definiteCross)
1019
+ let authoredMain = (horizontal ? style.width : style.height)?.resolve(definiteMain)
1020
+ let authoredCross = (horizontal ? style.height : style.width)?.resolve(definiteCross)
1021
+ let explicitMain = authoredMain ?? style.aspectRatio.flatMap { ratio in
1022
+ authoredCross.map { horizontal ? $0 * ratio : $0 / ratio }
1023
+ }
1024
+ let explicitCross = authoredCross ?? style.aspectRatio.flatMap { ratio in
1025
+ authoredMain.map { horizontal ? $0 / ratio : $0 * ratio }
1026
+ }
969
1027
  let minMain = (horizontal ? style.minWidth : style.minHeight)?.resolve(definiteMain)
970
1028
  let maxMain = (horizontal ? style.maxWidth : style.maxHeight)?.resolve(definiteMain)
971
1029
  let minCross = (horizontal ? style.minHeight : style.minWidth)?.resolve(definiteCross)
@@ -1146,6 +1204,9 @@ private class CraftNativeFlowView: UIStackView {
1146
1204
  case "space-around":
1147
1205
  crossBetween = free / CGFloat(lineCrosses.count) + crossGap
1148
1206
  crossLeading = free / CGFloat(lineCrosses.count * 2)
1207
+ case "space-evenly":
1208
+ crossBetween = free / CGFloat(lineCrosses.count + 1) + crossGap
1209
+ crossLeading = free / CGFloat(lineCrosses.count + 1)
1149
1210
  case "stretch":
1150
1211
  let extra = free / CGFloat(lineCrosses.count)
1151
1212
  lineCrosses = lineCrosses.map { $0 + extra }
@@ -1242,11 +1303,14 @@ private class CraftNativeFlowView: UIStackView {
1242
1303
  let style = child.craftLayoutStyle
1243
1304
  let available = max(0, cellWidth - style.marginLeft - style.marginRight)
1244
1305
  let align = style.alignSelf ?? alignItems
1245
- let fixedWidth = style.width?.resolve(cellWidth) ?? (align == "stretch" ? available : nil)
1306
+ var fixedWidth = style.width?.resolve(cellWidth)
1307
+ var fixedHeight = style.height?.resolve(nil)
1308
+ if fixedWidth == nil && fixedHeight == nil && align == "stretch" { fixedWidth = available }
1309
+ (fixedWidth, fixedHeight) = aspectDimensions(style, width: fixedWidth, height: fixedHeight)
1246
1310
  let size = CraftNativeFlexLayout.measure(
1247
1311
  child,
1248
1312
  width: fixedWidth.map { min($0, available) },
1249
- height: style.height?.resolve(nil),
1313
+ height: fixedHeight,
1250
1314
  maxWidth: available,
1251
1315
  maxHeight: CraftNativeFlexLayout.unbounded
1252
1316
  )
@@ -1270,16 +1334,23 @@ private class CraftNativeFlowView: UIStackView {
1270
1334
  let align = style.alignSelf ?? alignItems
1271
1335
  let size = sizes[index]
1272
1336
  let alignment: UInt32 = align == "center" ? 1 : align == "flex-end" ? 2 : 0
1337
+ var explicitWidth = style.width?.resolve(cellWidth)
1338
+ var explicitHeight = style.height?.resolve(nil)
1339
+ if explicitWidth == nil && explicitHeight == nil && align == "stretch" {
1340
+ explicitWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
1341
+ }
1342
+ (explicitWidth, explicitHeight) = aspectDimensions(style, width: explicitWidth, height: explicitHeight)
1273
1343
  return (
1274
1344
  size.width, size.height,
1275
- style.width?.resolve(cellWidth), style.height?.resolve(nil),
1345
+ explicitWidth, explicitHeight,
1276
1346
  style.minWidth?.resolve(cellWidth), style.maxWidth?.resolve(cellWidth),
1277
1347
  style.minHeight?.resolve(nil), style.maxHeight?.resolve(nil),
1278
1348
  style.marginLeft, style.marginRight, style.marginTop, style.marginBottom,
1279
1349
  align == "stretch", alignment
1280
1350
  )
1281
1351
  },
1282
- autoRowHeight: gridAutoRows.map { $0.resolve(innerHeight) ?? 0 }
1352
+ autoRowHeight: gridAutoRows.map { $0.resolve(innerHeight) ?? 0 },
1353
+ alignContent: craftNativeAlignContentCode(alignContent)
1283
1354
  )
1284
1355
  if place {
1285
1356
  if let sharedFrames {
@@ -1289,24 +1360,41 @@ private class CraftNativeFlowView: UIStackView {
1289
1360
  }
1290
1361
  return CGSize(width: width, height: innerHeight ?? height)
1291
1362
  }
1292
- var rowTop = padding.top
1363
+ let gridHeight = innerHeight ?? height
1364
+ let gridGaps = CGFloat(max(0, rowHeights.count - 1)) * verticalGap
1365
+ let gridFree = max(0, gridHeight - rowHeights.reduce(0, +) - gridGaps)
1366
+ var rowLeading: CGFloat = 0
1367
+ var rowBetween = verticalGap
1368
+ var rowExtra: CGFloat = 0
1369
+ switch alignContent {
1370
+ case "center": rowLeading = gridFree / 2
1371
+ case "flex-end": rowLeading = gridFree
1372
+ case "space-between":
1373
+ if rowHeights.count > 1 { rowBetween += gridFree / CGFloat(rowHeights.count - 1) }
1374
+ case "space-around":
1375
+ rowLeading = gridFree / CGFloat(rowHeights.count * 2)
1376
+ rowBetween += gridFree / CGFloat(rowHeights.count)
1377
+ case "stretch": rowExtra = gridFree / CGFloat(max(1, rowHeights.count))
1378
+ default: break
1379
+ }
1380
+ var rowTop = padding.top + rowLeading
1293
1381
  for (index, child) in children.enumerated() {
1294
1382
  let style = child.craftLayoutStyle
1295
1383
  let row = index / columns
1296
1384
  let column = index % columns
1297
- let rowHeight = rowHeights[row]
1385
+ let rowHeight = rowHeights[row] + rowExtra
1298
1386
  let size = sizes[index]
1299
1387
  let align = style.alignSelf ?? alignItems
1300
1388
  let availableWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
1301
1389
  let availableHeight = max(0, rowHeight - style.marginTop - style.marginBottom)
1302
1390
  let childWidth = min(availableWidth, size.width)
1303
- let childHeight = align == "stretch" && style.height == nil ? availableHeight : min(availableHeight, size.height)
1391
+ let childHeight = align == "stretch" && style.height == nil && style.aspectRatio == nil ? availableHeight : min(availableHeight, size.height)
1304
1392
  let x = padding.left + CGFloat(column) * (cellWidth + horizontalGap) + style.marginLeft
1305
1393
  + (align == "center" ? (availableWidth - childWidth) / 2 : align == "flex-end" ? availableWidth - childWidth : 0)
1306
1394
  let y = rowTop + style.marginTop
1307
1395
  + (align == "center" ? (availableHeight - childHeight) / 2 : align == "flex-end" ? availableHeight - childHeight : 0)
1308
1396
  frames.append((child, CGRect(x: x, y: y, width: childWidth, height: childHeight)))
1309
- if column == columns - 1 || index == children.count - 1 { rowTop += rowHeight + verticalGap }
1397
+ if column == columns - 1 || index == children.count - 1 { rowTop += rowHeight + rowBetween }
1310
1398
  }
1311
1399
  }
1312
1400
  return CGSize(width: width, height: innerHeight ?? height)
@@ -3581,7 +3669,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3581
3669
  current.protocolId = node["id"] as? String
3582
3670
  current.style = style
3583
3671
  result.accessibilityIdentifier = accessibilityIdentifier(node, props: props) ?? path
3584
- if type != "View" && type != "SafeAreaView" && type != "KeyboardAvoidingView" && type != "ScrollView" && type != "FlatList" && type != "Modal" {
3672
+ if type != "View" && type != "SafeAreaView" && type != "KeyboardAvoidingView" && type != "ScrollView" && type != "SectionList" && type != "FlatList" && type != "Modal" {
3585
3673
  result.setContentHuggingPriority(.required, for: .vertical)
3586
3674
  }
3587
3675
  applyViewStyle(style, to: result, node: current)
@@ -3893,7 +3981,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3893
3981
  controls: (props["controls"] as? Bool) == true,
3894
3982
  resizeMode: style["resizeMode"] as? String ?? props["resizeMode"] as? String
3895
3983
  )
3896
- case "ScrollView":
3984
+ case "ScrollView", "SectionList":
3897
3985
  let scroll = result as! CraftNativeScrollView
3898
3986
  let flexDirection = style["flexDirection"] as? String
3899
3987
  let direction = (props["horizontal"] as? Bool) == true
@@ -4020,7 +4108,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4020
4108
  let icon = CraftNativeIconView()
4021
4109
  icon.contentMode = .scaleAspectFit
4022
4110
  return icon
4023
- case "ScrollView":
4111
+ case "ScrollView", "SectionList":
4024
4112
  return CraftNativeScrollView()
4025
4113
  case "FlatList":
4026
4114
  return CraftNativeFlatList()
@@ -4055,6 +4143,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4055
4143
  case "video": return "Video"
4056
4144
  case "icon": return "Icon"
4057
4145
  case "scrollview": return "ScrollView"
4146
+ case "sectionlist": return "SectionList"
4058
4147
  case "flatlist": return "FlatList"
4059
4148
  case "svg": return "Svg"
4060
4149
  case "touchableopacity": return "TouchableOpacity"
package/dist/mobile.js CHANGED
@@ -1348,6 +1348,18 @@ var health = {
1348
1348
  if (!craft?.health?.getSleepIntervals)
1349
1349
  throw new Error("Reading sleep intervals is unavailable");
1350
1350
  return craft.health.getSleepIntervals(options);
1351
+ },
1352
+ async saveMindfulSession(session) {
1353
+ const craft = getCraftRoot();
1354
+ if (!craft?.health?.saveMindfulSession)
1355
+ throw new Error("Saving mindful sessions is unavailable");
1356
+ return craft.health.saveMindfulSession(session);
1357
+ },
1358
+ async getMindfulSessions(options = {}) {
1359
+ const craft = getCraftRoot();
1360
+ if (!craft?.health?.getMindfulSessions)
1361
+ throw new Error("Reading mindful sessions is unavailable");
1362
+ return craft.health.getMindfulSessions(options);
1351
1363
  }
1352
1364
  };
1353
1365
  async function updateLiveActivity(idOrState, state) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.160",
3
+ "version": "0.0.162",
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>",