craft-native 0.0.160 → 0.0.161

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)
@@ -772,6 +776,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
772
776
  listOf(if (align == "stretch") 1f else 0f, if (align == "center") 1f else if (align == "flex-end") 2f else 0f)
773
777
  }.toFloatArray(),
774
778
  gridAutoRows?.resolve(contentHeight)?.toFloat(),
779
+ alignContentCode(),
775
780
  )
776
781
  if (sharedFrames != null) {
777
782
  for ((index, entry) in flow.withIndex()) {
@@ -783,9 +788,26 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
783
788
  entry.view.layout(x, y, x + childWidth, y + childHeight)
784
789
  }
785
790
  } else {
786
- var rowTop = paddingTop
791
+ val totalRows = layout.rowHeights.sum()
792
+ val rowGaps = max(0, layout.rowHeights.size - 1) * rowGap
793
+ val freeRows = max(0, contentHeight - totalRows - rowGaps)
794
+ val rowCount = max(1, layout.rowHeights.size)
795
+ var rowLeading = 0
796
+ var rowBetween = rowGap
797
+ var rowExtra = 0
798
+ when (alignContentCode()) {
799
+ 1 -> rowLeading = freeRows / 2
800
+ 2 -> rowLeading = freeRows
801
+ 3 -> if (rowCount > 1) rowBetween += freeRows / (rowCount - 1)
802
+ 4 -> {
803
+ rowLeading = freeRows / (rowCount * 2)
804
+ rowBetween += freeRows / rowCount
805
+ }
806
+ 5 -> rowExtra = freeRows / rowCount
807
+ }
808
+ var rowTop = paddingTop + rowLeading
787
809
  for ((rowIndex, row) in layout.rows.withIndex()) {
788
- val rowHeight = layout.rowHeights[rowIndex]
810
+ val rowHeight = layout.rowHeights[rowIndex] + rowExtra
789
811
  for ((column, entry) in row.withIndex()) {
790
812
  val align = entry.style.alignSelf ?: alignItems
791
813
  val lp = entry.view.layoutParams as? MarginLayoutParams
@@ -809,7 +831,7 @@ private class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
809
831
  }
810
832
  entry.view.layout(x, y, x + childWidth, y + childHeight)
811
833
  }
812
- rowTop += rowHeight + rowGap
834
+ rowTop += rowHeight + rowBetween
813
835
  }
814
836
  }
815
837
  for (entry in layout.absolute) {
@@ -2803,7 +2825,7 @@ class MainActivity : AppCompatActivity() {
2803
2825
  "Svg" -> (previous as? CraftNativeSvgView ?: CraftNativeSvgView(this)).apply {
2804
2826
  drawing = node
2805
2827
  }
2806
- "ScrollView" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
2828
+ "ScrollView", "SectionList" -> (previous as? CraftNativeScrollView ?: CraftNativeScrollView(this)).apply {
2807
2829
  val horizontal = props.optBoolean("horizontal", false) || style.optString("flexDirection") in setOf("row", "row-reverse")
2808
2830
  setHorizontal(horizontal)
2809
2831
  setNativeNestedScrollingEnabled(true)
@@ -3064,6 +3086,7 @@ class MainActivity : AppCompatActivity() {
3064
3086
  "video" -> "Video"
3065
3087
  "icon" -> "Icon"
3066
3088
  "scrollview" -> "ScrollView"
3089
+ "sectionlist" -> "SectionList"
3067
3090
  "flatlist" -> "FlatList"
3068
3091
  "svg" -> "Svg"
3069
3092
  "touchableopacity" -> "TouchableOpacity"
@@ -3477,7 +3500,9 @@ class MainActivity : AppCompatActivity() {
3477
3500
  else -> null
3478
3501
  }
3479
3502
  view.tooltipText = props.optString("accessibilityHint").takeIf { it.isNotBlank() }
3480
- val role = props.optString("accessibilityRole").ifBlank { type.lowercase() }
3503
+ val role = props.optString("accessibilityRole").ifBlank {
3504
+ if (type == "Icon") "image" else type.lowercase()
3505
+ }
3481
3506
  val state = props.optJSONObject("accessibilityState")
3482
3507
  val disabled = props.optBoolean("disabled", false) || state?.optBoolean("disabled", false) == true
3483
3508
  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.161";
3307
3307
 
3308
3308
  // bin/cli.ts
3309
3309
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
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.161";
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.161";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -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,17 @@ 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
+ default: return 0
267
+ }
268
+ }
269
+
238
270
  private final class CraftNativeVideoView: UIView {
239
271
  private let playerController = AVPlayerViewController()
240
272
  private var player: AVPlayer?
@@ -436,6 +468,7 @@ private enum CraftNativeLength: Equatable {
436
468
  private struct CraftNativeLayoutStyle {
437
469
  let width: CraftNativeLength?
438
470
  let height: CraftNativeLength?
471
+ let aspectRatio: CGFloat?
439
472
  let minWidth: CraftNativeLength?
440
473
  let maxWidth: CraftNativeLength?
441
474
  let minHeight: CraftNativeLength?
@@ -465,6 +498,8 @@ private struct CraftNativeLayoutStyle {
465
498
  func number(_ value: Any?) -> CGFloat? { (value as? NSNumber).map { CGFloat(truncating: $0) } }
466
499
  width = CraftNativeLength(raw["width"])
467
500
  height = CraftNativeLength(raw["height"])
501
+ let ratio = number(raw["aspectRatio"])
502
+ aspectRatio = ratio.flatMap { $0.isFinite && $0 > 0 ? $0 : nil }
468
503
  minWidth = CraftNativeLength(raw["minWidth"])
469
504
  maxWidth = CraftNativeLength(raw["maxWidth"])
470
505
  minHeight = CraftNativeLength(raw["minHeight"])
@@ -870,12 +905,13 @@ private class CraftNativeFlowView: UIStackView {
870
905
  let content = CGRect(x: padding.left, y: padding.top, width: max(0, size.width - paddingWidth), height: max(0, size.height - paddingHeight))
871
906
  for child in children where child.craftLayoutStyle.position == "absolute" {
872
907
  let style = child.craftLayoutStyle
873
- let fixedWidth = style.left != nil && style.right != nil
908
+ var fixedWidth = style.left != nil && style.right != nil
874
909
  ? max(0, content.width - (style.left ?? 0) - (style.right ?? 0))
875
910
  : style.width?.resolve(content.width)
876
- let fixedHeight = style.top != nil && style.bottom != nil
911
+ var fixedHeight = style.top != nil && style.bottom != nil
877
912
  ? max(0, content.height - (style.top ?? 0) - (style.bottom ?? 0))
878
913
  : style.height?.resolve(content.height)
914
+ (fixedWidth, fixedHeight) = aspectDimensions(style, width: fixedWidth, height: fixedHeight)
879
915
  let measured = CraftNativeFlexLayout.measure(child, width: fixedWidth, height: fixedHeight, maxWidth: content.width, maxHeight: content.height)
880
916
  let x = style.left ?? (style.right.map { content.width - $0 - measured.width } ?? 0)
881
917
  let y = style.top ?? (style.bottom.map { content.height - $0 - measured.height } ?? 0)
@@ -885,6 +921,21 @@ private class CraftNativeFlowView: UIStackView {
885
921
  return (size, frames)
886
922
  }
887
923
 
924
+ /// Resolves the missing side of an aspect-ratio box after its parent has
925
+ /// supplied any definite width or height. Keeping this at the host edge
926
+ /// lets grid and absolute children follow the same rule as flex items
927
+ /// without adding platform-specific measurement behavior to native views.
928
+ private func aspectDimensions(
929
+ _ style: CraftNativeLayoutStyle,
930
+ width: CGFloat?,
931
+ height: CGFloat?
932
+ ) -> (width: CGFloat?, height: CGFloat?) {
933
+ guard let ratio = style.aspectRatio else { return (width, height) }
934
+ if let width, height == nil { return (width, width / ratio) }
935
+ if let height, width == nil { return (height * ratio, height) }
936
+ return (width, height)
937
+ }
938
+
888
939
  private func layoutFlex(
889
940
  _ children: [UIView],
890
941
  horizontal: Bool,
@@ -964,8 +1015,14 @@ private class CraftNativeFlowView: UIStackView {
964
1015
  let crossLeading = horizontal ? style.marginTop : style.marginLeft
965
1016
  let crossTrailing = horizontal ? style.marginBottom : style.marginRight
966
1017
  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)
1018
+ let authoredMain = (horizontal ? style.width : style.height)?.resolve(definiteMain)
1019
+ let authoredCross = (horizontal ? style.height : style.width)?.resolve(definiteCross)
1020
+ let explicitMain = authoredMain ?? style.aspectRatio.flatMap { ratio in
1021
+ authoredCross.map { horizontal ? $0 * ratio : $0 / ratio }
1022
+ }
1023
+ let explicitCross = authoredCross ?? style.aspectRatio.flatMap { ratio in
1024
+ authoredMain.map { horizontal ? $0 / ratio : $0 * ratio }
1025
+ }
969
1026
  let minMain = (horizontal ? style.minWidth : style.minHeight)?.resolve(definiteMain)
970
1027
  let maxMain = (horizontal ? style.maxWidth : style.maxHeight)?.resolve(definiteMain)
971
1028
  let minCross = (horizontal ? style.minHeight : style.minWidth)?.resolve(definiteCross)
@@ -1242,11 +1299,14 @@ private class CraftNativeFlowView: UIStackView {
1242
1299
  let style = child.craftLayoutStyle
1243
1300
  let available = max(0, cellWidth - style.marginLeft - style.marginRight)
1244
1301
  let align = style.alignSelf ?? alignItems
1245
- let fixedWidth = style.width?.resolve(cellWidth) ?? (align == "stretch" ? available : nil)
1302
+ var fixedWidth = style.width?.resolve(cellWidth)
1303
+ var fixedHeight = style.height?.resolve(nil)
1304
+ if fixedWidth == nil && fixedHeight == nil && align == "stretch" { fixedWidth = available }
1305
+ (fixedWidth, fixedHeight) = aspectDimensions(style, width: fixedWidth, height: fixedHeight)
1246
1306
  let size = CraftNativeFlexLayout.measure(
1247
1307
  child,
1248
1308
  width: fixedWidth.map { min($0, available) },
1249
- height: style.height?.resolve(nil),
1309
+ height: fixedHeight,
1250
1310
  maxWidth: available,
1251
1311
  maxHeight: CraftNativeFlexLayout.unbounded
1252
1312
  )
@@ -1270,16 +1330,23 @@ private class CraftNativeFlowView: UIStackView {
1270
1330
  let align = style.alignSelf ?? alignItems
1271
1331
  let size = sizes[index]
1272
1332
  let alignment: UInt32 = align == "center" ? 1 : align == "flex-end" ? 2 : 0
1333
+ var explicitWidth = style.width?.resolve(cellWidth)
1334
+ var explicitHeight = style.height?.resolve(nil)
1335
+ if explicitWidth == nil && explicitHeight == nil && align == "stretch" {
1336
+ explicitWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
1337
+ }
1338
+ (explicitWidth, explicitHeight) = aspectDimensions(style, width: explicitWidth, height: explicitHeight)
1273
1339
  return (
1274
1340
  size.width, size.height,
1275
- style.width?.resolve(cellWidth), style.height?.resolve(nil),
1341
+ explicitWidth, explicitHeight,
1276
1342
  style.minWidth?.resolve(cellWidth), style.maxWidth?.resolve(cellWidth),
1277
1343
  style.minHeight?.resolve(nil), style.maxHeight?.resolve(nil),
1278
1344
  style.marginLeft, style.marginRight, style.marginTop, style.marginBottom,
1279
1345
  align == "stretch", alignment
1280
1346
  )
1281
1347
  },
1282
- autoRowHeight: gridAutoRows.map { $0.resolve(innerHeight) ?? 0 }
1348
+ autoRowHeight: gridAutoRows.map { $0.resolve(innerHeight) ?? 0 },
1349
+ alignContent: craftNativeAlignContentCode(alignContent)
1283
1350
  )
1284
1351
  if place {
1285
1352
  if let sharedFrames {
@@ -1289,24 +1356,41 @@ private class CraftNativeFlowView: UIStackView {
1289
1356
  }
1290
1357
  return CGSize(width: width, height: innerHeight ?? height)
1291
1358
  }
1292
- var rowTop = padding.top
1359
+ let gridHeight = innerHeight ?? height
1360
+ let gridGaps = CGFloat(max(0, rowHeights.count - 1)) * verticalGap
1361
+ let gridFree = max(0, gridHeight - rowHeights.reduce(0, +) - gridGaps)
1362
+ var rowLeading: CGFloat = 0
1363
+ var rowBetween = verticalGap
1364
+ var rowExtra: CGFloat = 0
1365
+ switch alignContent {
1366
+ case "center": rowLeading = gridFree / 2
1367
+ case "flex-end": rowLeading = gridFree
1368
+ case "space-between":
1369
+ if rowHeights.count > 1 { rowBetween += gridFree / CGFloat(rowHeights.count - 1) }
1370
+ case "space-around":
1371
+ rowLeading = gridFree / CGFloat(rowHeights.count * 2)
1372
+ rowBetween += gridFree / CGFloat(rowHeights.count)
1373
+ case "stretch": rowExtra = gridFree / CGFloat(max(1, rowHeights.count))
1374
+ default: break
1375
+ }
1376
+ var rowTop = padding.top + rowLeading
1293
1377
  for (index, child) in children.enumerated() {
1294
1378
  let style = child.craftLayoutStyle
1295
1379
  let row = index / columns
1296
1380
  let column = index % columns
1297
- let rowHeight = rowHeights[row]
1381
+ let rowHeight = rowHeights[row] + rowExtra
1298
1382
  let size = sizes[index]
1299
1383
  let align = style.alignSelf ?? alignItems
1300
1384
  let availableWidth = max(0, cellWidth - style.marginLeft - style.marginRight)
1301
1385
  let availableHeight = max(0, rowHeight - style.marginTop - style.marginBottom)
1302
1386
  let childWidth = min(availableWidth, size.width)
1303
- let childHeight = align == "stretch" && style.height == nil ? availableHeight : min(availableHeight, size.height)
1387
+ let childHeight = align == "stretch" && style.height == nil && style.aspectRatio == nil ? availableHeight : min(availableHeight, size.height)
1304
1388
  let x = padding.left + CGFloat(column) * (cellWidth + horizontalGap) + style.marginLeft
1305
1389
  + (align == "center" ? (availableWidth - childWidth) / 2 : align == "flex-end" ? availableWidth - childWidth : 0)
1306
1390
  let y = rowTop + style.marginTop
1307
1391
  + (align == "center" ? (availableHeight - childHeight) / 2 : align == "flex-end" ? availableHeight - childHeight : 0)
1308
1392
  frames.append((child, CGRect(x: x, y: y, width: childWidth, height: childHeight)))
1309
- if column == columns - 1 || index == children.count - 1 { rowTop += rowHeight + verticalGap }
1393
+ if column == columns - 1 || index == children.count - 1 { rowTop += rowHeight + rowBetween }
1310
1394
  }
1311
1395
  }
1312
1396
  return CGSize(width: width, height: innerHeight ?? height)
@@ -3581,7 +3665,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3581
3665
  current.protocolId = node["id"] as? String
3582
3666
  current.style = style
3583
3667
  result.accessibilityIdentifier = accessibilityIdentifier(node, props: props) ?? path
3584
- if type != "View" && type != "SafeAreaView" && type != "KeyboardAvoidingView" && type != "ScrollView" && type != "FlatList" && type != "Modal" {
3668
+ if type != "View" && type != "SafeAreaView" && type != "KeyboardAvoidingView" && type != "ScrollView" && type != "SectionList" && type != "FlatList" && type != "Modal" {
3585
3669
  result.setContentHuggingPriority(.required, for: .vertical)
3586
3670
  }
3587
3671
  applyViewStyle(style, to: result, node: current)
@@ -3893,7 +3977,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
3893
3977
  controls: (props["controls"] as? Bool) == true,
3894
3978
  resizeMode: style["resizeMode"] as? String ?? props["resizeMode"] as? String
3895
3979
  )
3896
- case "ScrollView":
3980
+ case "ScrollView", "SectionList":
3897
3981
  let scroll = result as! CraftNativeScrollView
3898
3982
  let flexDirection = style["flexDirection"] as? String
3899
3983
  let direction = (props["horizontal"] as? Bool) == true
@@ -4020,7 +4104,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4020
4104
  let icon = CraftNativeIconView()
4021
4105
  icon.contentMode = .scaleAspectFit
4022
4106
  return icon
4023
- case "ScrollView":
4107
+ case "ScrollView", "SectionList":
4024
4108
  return CraftNativeScrollView()
4025
4109
  case "FlatList":
4026
4110
  return CraftNativeFlatList()
@@ -4055,6 +4139,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
4055
4139
  case "video": return "Video"
4056
4140
  case "icon": return "Icon"
4057
4141
  case "scrollview": return "ScrollView"
4142
+ case "sectionlist": return "SectionList"
4058
4143
  case "flatlist": return "FlatList"
4059
4144
  case "svg": return "Svg"
4060
4145
  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.161",
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>",