craft-native 0.0.164 → 0.0.166
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/templates/AndroidManifest.xml.template +1 -1
- package/dist/android/templates/MainActivityNative.kt.template +416 -36
- package/dist/api/mobile.d.ts +25 -1
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +1 -0
- package/dist/ios/src/index.js +138 -88
- package/dist/ios/src/source-audit.d.ts +8 -0
- package/dist/ios/templates/CraftApp.swift +63 -2
- package/dist/ios/templates/CraftHealth.swift +214 -39
- package/dist/ios/templates/CraftHeartRate.swift +34 -4
- package/dist/ios/templates/CraftHybrid.swift +140 -17
- package/dist/ios/templates/CraftNativeScreen.swift +320 -56
- package/dist/mobile.js +29 -0
- package/package.json +1 -1
|
@@ -550,6 +550,27 @@ private final class CraftNativeLayoutStyleBox {
|
|
|
550
550
|
|
|
551
551
|
private var craftNativeLayoutStyleKey: UInt8 = 0
|
|
552
552
|
private var craftNativeCornerRadiusKey: UInt8 = 0
|
|
553
|
+
private var craftNativeCornerRadiiKey: UInt8 = 0
|
|
554
|
+
private var craftNativeBorderLayerKey: UInt8 = 0
|
|
555
|
+
private var craftNativeSideBorderLayersKey: UInt8 = 0
|
|
556
|
+
|
|
557
|
+
private func craftRoundedPath(_ rect: CGRect, _ rawRadii: [CGFloat]) -> CGPath {
|
|
558
|
+
let limit = min(rect.width, rect.height) / 2
|
|
559
|
+
let radii = (0..<4).map { min(max(0, rawRadii.count > $0 ? rawRadii[$0] : 0), limit) }
|
|
560
|
+
let path = CGMutablePath()
|
|
561
|
+
let minX = rect.minX, maxX = rect.maxX, minY = rect.minY, maxY = rect.maxY
|
|
562
|
+
path.move(to: CGPoint(x: minX + radii[0], y: minY))
|
|
563
|
+
path.addLine(to: CGPoint(x: maxX - radii[1], y: minY))
|
|
564
|
+
path.addArc(center: CGPoint(x: maxX - radii[1], y: minY + radii[1]), radius: radii[1], startAngle: -.pi / 2, endAngle: 0, clockwise: false)
|
|
565
|
+
path.addLine(to: CGPoint(x: maxX, y: maxY - radii[2]))
|
|
566
|
+
path.addArc(center: CGPoint(x: maxX - radii[2], y: maxY - radii[2]), radius: radii[2], startAngle: 0, endAngle: .pi / 2, clockwise: false)
|
|
567
|
+
path.addLine(to: CGPoint(x: minX + radii[3], y: maxY))
|
|
568
|
+
path.addArc(center: CGPoint(x: minX + radii[3], y: maxY - radii[3]), radius: radii[3], startAngle: .pi / 2, endAngle: .pi, clockwise: false)
|
|
569
|
+
path.addLine(to: CGPoint(x: minX, y: minY + radii[0]))
|
|
570
|
+
path.addArc(center: CGPoint(x: minX + radii[0], y: minY + radii[0]), radius: radii[0], startAngle: .pi, endAngle: .pi * 1.5, clockwise: false)
|
|
571
|
+
path.closeSubpath()
|
|
572
|
+
return path
|
|
573
|
+
}
|
|
553
574
|
|
|
554
575
|
extension UIView {
|
|
555
576
|
/// The layout style this view's parent reads. It lives on the view, so an
|
|
@@ -571,11 +592,98 @@ extension UIView {
|
|
|
571
592
|
}
|
|
572
593
|
}
|
|
573
594
|
|
|
595
|
+
var craftRequestedCornerRadii: [CGFloat]? {
|
|
596
|
+
get { (objc_getAssociatedObject(self, &craftNativeCornerRadiiKey) as? [NSNumber])?.map { CGFloat(truncating: $0) } }
|
|
597
|
+
set {
|
|
598
|
+
objc_setAssociatedObject(self, &craftNativeCornerRadiiKey, newValue?.map { NSNumber(value: Double($0)) }, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
599
|
+
craftApplyCornerRadius()
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
574
603
|
func craftApplyCornerRadius() {
|
|
575
604
|
let requested = craftRequestedCornerRadius ?? 0
|
|
576
605
|
let limit = min(bounds.width, bounds.height) / 2
|
|
577
606
|
let radius = limit > 0 ? min(requested, limit) : min(requested, 9_998)
|
|
578
|
-
if
|
|
607
|
+
if let radii = craftRequestedCornerRadii {
|
|
608
|
+
let mask = (layer.mask as? CAShapeLayer) ?? CAShapeLayer()
|
|
609
|
+
mask.frame = bounds
|
|
610
|
+
mask.path = craftRoundedPath(bounds, radii)
|
|
611
|
+
layer.mask = mask
|
|
612
|
+
layer.cornerRadius = 0
|
|
613
|
+
} else {
|
|
614
|
+
layer.mask = nil
|
|
615
|
+
if layer.cornerRadius != radius { layer.cornerRadius = radius }
|
|
616
|
+
}
|
|
617
|
+
if let border = objc_getAssociatedObject(self, &craftNativeBorderLayerKey) as? CAShapeLayer {
|
|
618
|
+
border.frame = bounds
|
|
619
|
+
let inset = border.lineWidth / 2
|
|
620
|
+
border.path = UIBezierPath(
|
|
621
|
+
roundedRect: bounds.insetBy(dx: inset, dy: inset),
|
|
622
|
+
cornerRadius: max(0, radius - inset)
|
|
623
|
+
).cgPath
|
|
624
|
+
}
|
|
625
|
+
if let sides = objc_getAssociatedObject(self, &craftNativeSideBorderLayersKey) as? [CALayer], sides.count == 4 {
|
|
626
|
+
let top = sides[0].frame.height
|
|
627
|
+
let right = sides[1].frame.width
|
|
628
|
+
let bottom = sides[2].frame.height
|
|
629
|
+
let left = sides[3].frame.width
|
|
630
|
+
sides[0].frame = CGRect(x: 0, y: 0, width: bounds.width, height: top)
|
|
631
|
+
sides[1].frame = CGRect(x: max(0, bounds.width - right), y: 0, width: right, height: bounds.height)
|
|
632
|
+
sides[2].frame = CGRect(x: 0, y: max(0, bounds.height - bottom), width: bounds.width, height: bottom)
|
|
633
|
+
sides[3].frame = CGRect(x: 0, y: 0, width: left, height: bounds.height)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
fileprivate func craftApplyBorderStyle(_ style: String?, width: CGFloat, color: UIColor) {
|
|
638
|
+
guard width > 0, style == "dashed" || style == "dotted" else {
|
|
639
|
+
(objc_getAssociatedObject(self, &craftNativeBorderLayerKey) as? CAShapeLayer)?.removeFromSuperlayer()
|
|
640
|
+
objc_setAssociatedObject(self, &craftNativeBorderLayerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
641
|
+
layer.borderWidth = width
|
|
642
|
+
layer.borderColor = color.cgColor
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
layer.borderWidth = 0
|
|
646
|
+
let border = (objc_getAssociatedObject(self, &craftNativeBorderLayerKey) as? CAShapeLayer) ?? CAShapeLayer()
|
|
647
|
+
border.strokeColor = color.cgColor
|
|
648
|
+
border.fillColor = nil
|
|
649
|
+
border.lineWidth = width
|
|
650
|
+
border.lineDashPattern = style == "dotted"
|
|
651
|
+
? [NSNumber(value: Double(width)), NSNumber(value: Double(width * 1.5))]
|
|
652
|
+
: [NSNumber(value: Double(width * 3)), NSNumber(value: Double(width * 2))]
|
|
653
|
+
border.contentsScale = window?.screen.scale ?? traitCollection.displayScale
|
|
654
|
+
if border.superlayer !== layer { layer.addSublayer(border) }
|
|
655
|
+
objc_setAssociatedObject(self, &craftNativeBorderLayerKey, border, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
656
|
+
craftApplyCornerRadius()
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
fileprivate func craftApplySideBorders(_ hasSideBorders: Bool, style: String?, widths: [CGFloat], colors: [UIColor]) {
|
|
660
|
+
guard hasSideBorders else {
|
|
661
|
+
(objc_getAssociatedObject(self, &craftNativeSideBorderLayersKey) as? [CALayer])?.forEach { $0.removeFromSuperlayer() }
|
|
662
|
+
objc_setAssociatedObject(self, &craftNativeSideBorderLayersKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
663
|
+
craftApplyBorderStyle(style, width: widths.first ?? 0, color: colors.first ?? .clear)
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
(objc_getAssociatedObject(self, &craftNativeBorderLayerKey) as? CAShapeLayer)?.removeFromSuperlayer()
|
|
667
|
+
objc_setAssociatedObject(self, &craftNativeBorderLayerKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
668
|
+
layer.borderWidth = 0
|
|
669
|
+
let top = max(0, widths.count > 0 ? widths[0] : 0)
|
|
670
|
+
let right = max(0, widths.count > 1 ? widths[1] : 0)
|
|
671
|
+
let bottom = max(0, widths.count > 2 ? widths[2] : 0)
|
|
672
|
+
let left = max(0, widths.count > 3 ? widths[3] : 0)
|
|
673
|
+
let frames = [
|
|
674
|
+
CGRect(x: 0, y: 0, width: bounds.width, height: top),
|
|
675
|
+
CGRect(x: max(0, bounds.width - right), y: 0, width: right, height: bounds.height),
|
|
676
|
+
CGRect(x: 0, y: max(0, bounds.height - bottom), width: bounds.width, height: bottom),
|
|
677
|
+
CGRect(x: 0, y: 0, width: left, height: bounds.height),
|
|
678
|
+
]
|
|
679
|
+
let layers = frames.enumerated().map { index, frame in
|
|
680
|
+
let side = CALayer()
|
|
681
|
+
side.frame = frame
|
|
682
|
+
side.backgroundColor = (colors.count > index ? colors[index] : .clear).cgColor
|
|
683
|
+
layer.addSublayer(side)
|
|
684
|
+
return side
|
|
685
|
+
}
|
|
686
|
+
objc_setAssociatedObject(self, &craftNativeSideBorderLayersKey, layers, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
579
687
|
}
|
|
580
688
|
}
|
|
581
689
|
|
|
@@ -670,6 +778,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
670
778
|
var gridColumns = 1 { didSet { if gridColumns != oldValue { invalidateMeasurements() } } }
|
|
671
779
|
var gridAutoRows: CraftNativeLength? { didSet { if gridAutoRows != oldValue { invalidateMeasurements() } } }
|
|
672
780
|
var reverse = false { didSet { if reverse != oldValue { invalidateMeasurements() } } }
|
|
781
|
+
var wrapReverse = false { didSet { if wrapReverse != oldValue { invalidateMeasurements() } } }
|
|
673
782
|
var padding = UIEdgeInsets.zero {
|
|
674
783
|
didSet {
|
|
675
784
|
layoutMargins = padding
|
|
@@ -869,6 +978,29 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
869
978
|
return max(0, result)
|
|
870
979
|
}
|
|
871
980
|
|
|
981
|
+
/// The first baseline is the cross-axis anchor for a horizontal baseline
|
|
982
|
+
/// flex line. Views without a baseline participate at their bottom edge,
|
|
983
|
+
/// matching the CSS fallback for replaced and non-text elements.
|
|
984
|
+
private static func firstBaseline(_ view: UIView, cross: CGFloat) -> CGFloat {
|
|
985
|
+
let baseline: CGFloat?
|
|
986
|
+
if let label = view as? CraftNativeLabel {
|
|
987
|
+
// CraftNativeLabel draws inside its inset rect, so include the
|
|
988
|
+
// authored top padding before the font's own baseline (UIKit's
|
|
989
|
+
// labels expose no measured first-baseline offset).
|
|
990
|
+
baseline = label.insets.top + label.font.ascender
|
|
991
|
+
} else if let label = view as? UILabel {
|
|
992
|
+
baseline = label.font.ascender
|
|
993
|
+
} else if let field = view as? UITextField {
|
|
994
|
+
baseline = field.font?.ascender
|
|
995
|
+
} else if let textView = view as? UITextView {
|
|
996
|
+
baseline = textView.font?.ascender
|
|
997
|
+
} else {
|
|
998
|
+
baseline = nil
|
|
999
|
+
}
|
|
1000
|
+
guard let baseline, baseline.isFinite else { return cross }
|
|
1001
|
+
return min(max(0, baseline), cross)
|
|
1002
|
+
}
|
|
1003
|
+
|
|
872
1004
|
/// Children that take part in the flow: shown, not a modal overlay.
|
|
873
1005
|
private var visibleChildren: [UIView] {
|
|
874
1006
|
flowChildren.filter { $0.superview === self && !$0.isHidden && !($0 is CraftNativeModalView) }
|
|
@@ -1255,31 +1387,53 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
1255
1387
|
}
|
|
1256
1388
|
let lineCross = lineCrosses[lineIndex]
|
|
1257
1389
|
let lineOffset = sharedCrossOffsets?[lineIndex] ?? crossOffset
|
|
1390
|
+
let crossReversed = wrapReverse && wrap
|
|
1391
|
+
let physicalLineOffset = crossReversed ? contentCross - lineOffset - lineCross : lineOffset
|
|
1258
1392
|
let sharedLine = sharedLines[lineIndex]
|
|
1259
1393
|
let useSharedPositions = !reverse && sharedLine != nil
|
|
1394
|
+
let baseline = horizontal
|
|
1395
|
+
? line.compactMap { index -> CGFloat? in
|
|
1396
|
+
let item = items[index]
|
|
1397
|
+
guard item.align == "baseline" else { return nil }
|
|
1398
|
+
return item.crossLeading + Self.firstBaseline(item.view, cross: item.cross)
|
|
1399
|
+
}.max()
|
|
1400
|
+
: nil
|
|
1260
1401
|
var mainOffset = leading
|
|
1261
1402
|
for (offset, index) in line.enumerated() {
|
|
1262
1403
|
let item = items[index]
|
|
1263
|
-
let
|
|
1264
|
-
let
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1404
|
+
let crossPosition: CGFloat
|
|
1405
|
+
if item.align == "baseline", let baseline {
|
|
1406
|
+
let childBaseline = Self.firstBaseline(item.view, cross: item.cross)
|
|
1407
|
+
crossPosition = CraftNativeCrossAxisBridge.position(
|
|
1408
|
+
available: baseline,
|
|
1409
|
+
size: childBaseline,
|
|
1410
|
+
leading: item.crossLeading,
|
|
1411
|
+
trailing: item.crossTrailing,
|
|
1412
|
+
alignment: 3
|
|
1413
|
+
) ?? max(item.crossLeading, baseline - childBaseline)
|
|
1414
|
+
} else {
|
|
1415
|
+
let alignment = item.align == "center" ? UInt32(1) : item.align == "flex-end" ? UInt32(2) : UInt32(0)
|
|
1416
|
+
crossPosition = CraftNativeCrossAxisBridge.position(
|
|
1417
|
+
available: lineCross,
|
|
1418
|
+
size: item.cross,
|
|
1419
|
+
leading: item.crossLeading,
|
|
1420
|
+
trailing: item.crossTrailing,
|
|
1421
|
+
alignment: alignment
|
|
1422
|
+
) ?? {
|
|
1423
|
+
switch alignment {
|
|
1424
|
+
case 1: return item.crossLeading + (lineCross - item.crossMargins - item.cross) / 2
|
|
1425
|
+
case 2: return lineCross - item.crossTrailing - item.cross
|
|
1426
|
+
default: return item.crossLeading
|
|
1427
|
+
}
|
|
1428
|
+
}()
|
|
1429
|
+
}
|
|
1430
|
+
let physicalCrossPosition = crossReversed ? lineCross - crossPosition - item.cross : crossPosition
|
|
1277
1431
|
let mainPosition = useSharedPositions
|
|
1278
1432
|
? (sharedLine?[offset].position ?? (mainOffset + item.mainLeading))
|
|
1279
1433
|
: mainOffset + item.mainLeading
|
|
1280
1434
|
let frame = horizontal
|
|
1281
|
-
? CGRect(x: padding.left + mainPosition, y: padding.top +
|
|
1282
|
-
: CGRect(x: padding.left +
|
|
1435
|
+
? CGRect(x: padding.left + mainPosition, y: padding.top + physicalLineOffset + physicalCrossPosition, width: item.main, height: item.cross)
|
|
1436
|
+
: CGRect(x: padding.left + physicalLineOffset + physicalCrossPosition, y: padding.top + mainPosition, width: item.cross, height: item.main)
|
|
1283
1437
|
frames.append((item.view, frame))
|
|
1284
1438
|
if !useSharedPositions { mainOffset += item.mainMargins + item.main + mainGap + between }
|
|
1285
1439
|
}
|
|
@@ -2448,9 +2602,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2448
2602
|
|
|
2449
2603
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
2450
2604
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
2451
|
-
|
|
2605
|
+
let colorChanged = previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle
|
|
2606
|
+
let textSizeChanged = previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory
|
|
2607
|
+
guard colorChanged || textSizeChanged else { return }
|
|
2452
2608
|
refreshTraitDefaults()
|
|
2453
|
-
|
|
2609
|
+
sendAppearance()
|
|
2454
2610
|
}
|
|
2455
2611
|
|
|
2456
2612
|
override func viewWillAppear(_ animated: Bool) {
|
|
@@ -2621,6 +2777,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2621
2777
|
capabilities: \(capabilityJSON),
|
|
2622
2778
|
initialAppState: "\(CraftNativeActions.currentAppState())",
|
|
2623
2779
|
colorScheme: "\(colorScheme)",
|
|
2780
|
+
appearance: \(appearanceJSON()),
|
|
2624
2781
|
appURL: \(appURL),
|
|
2625
2782
|
appVersion: \(appVersion),
|
|
2626
2783
|
appBuild: \(appBuild),
|
|
@@ -2701,6 +2858,18 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
2701
2858
|
return config.appearance == "dark" ? "dark" : "light"
|
|
2702
2859
|
}
|
|
2703
2860
|
|
|
2861
|
+
private func sendAppearance() {
|
|
2862
|
+
let style: UIUserInterfaceStyle = colorScheme == "dark" ? .dark : .light
|
|
2863
|
+
send(type: "APPEARANCE", payload: CraftPageAppearance.snapshot(style: style))
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
private func appearanceJSON() -> String {
|
|
2867
|
+
let style: UIUserInterfaceStyle = colorScheme == "dark" ? .dark : .light
|
|
2868
|
+
guard let data = try? JSONSerialization.data(withJSONObject: CraftPageAppearance.snapshot(style: style)),
|
|
2869
|
+
let json = String(data: data, encoding: .utf8) else { return "{}" }
|
|
2870
|
+
return json
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2704
2873
|
/// The screen's name as its script knows it: the route the host opened,
|
|
2705
2874
|
/// else the one a route bundle selected for itself.
|
|
2706
2875
|
private func screenName(in context: JSContext? = nil) -> String {
|
|
@@ -3203,6 +3372,16 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3203
3372
|
observe(UIApplication.didBecomeActiveNotification, state: "active")
|
|
3204
3373
|
observe(UIApplication.willResignActiveNotification, state: "inactive")
|
|
3205
3374
|
observe(UIApplication.didEnterBackgroundNotification, state: "background")
|
|
3375
|
+
for name in [UIContentSizeCategory.didChangeNotification, UIAccessibility.reduceMotionStatusDidChangeNotification, UIAccessibility.reduceTransparencyStatusDidChangeNotification, .craftColorSchemeChanged] {
|
|
3376
|
+
lifecycleObservers.append(NotificationCenter.default.addObserver(
|
|
3377
|
+
forName: name,
|
|
3378
|
+
object: nil,
|
|
3379
|
+
queue: .main
|
|
3380
|
+
) { [weak self] _ in
|
|
3381
|
+
self?.refreshTraitDefaults()
|
|
3382
|
+
self?.sendAppearance()
|
|
3383
|
+
})
|
|
3384
|
+
}
|
|
3206
3385
|
// The app script has no screen for the keyboard to cover.
|
|
3207
3386
|
if !isAppScript {
|
|
3208
3387
|
lifecycleObservers.append(NotificationCenter.default.addObserver(
|
|
@@ -4674,7 +4853,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4674
4853
|
let direction = style["flexDirection"] as? String
|
|
4675
4854
|
stack.axis = direction == "row" || direction == "row-reverse" ? .horizontal : .vertical
|
|
4676
4855
|
stack.reverse = direction?.hasSuffix("-reverse") == true
|
|
4677
|
-
|
|
4856
|
+
let flexWrap = style["flexWrap"] as? String
|
|
4857
|
+
stack.wrap = flexWrap == "wrap" || flexWrap == "wrap-reverse" || style["flexWrap"] as? Bool == true
|
|
4858
|
+
stack.wrapReverse = flexWrap == "wrap-reverse"
|
|
4678
4859
|
stack.alignItems = style["alignItems"] as? String ?? "stretch"
|
|
4679
4860
|
stack.alignContent = style["alignContent"] as? String ?? "flex-start"
|
|
4680
4861
|
stack.justifyContent = style["justifyContent"] as? String ?? "flex-start"
|
|
@@ -4710,14 +4891,37 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4710
4891
|
)
|
|
4711
4892
|
view.alpha = number(style["opacity"]) ?? 1
|
|
4712
4893
|
view.isHidden = style["display"] as? String == "none"
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4894
|
+
let borderRadius = number(style["borderRadius"]) ?? 0
|
|
4895
|
+
let cornerRadiusKeys = ["borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius"]
|
|
4896
|
+
let hasCornerRadii = cornerRadiusKeys.contains { style[$0] != nil }
|
|
4897
|
+
view.craftRequestedCornerRadius = borderRadius
|
|
4898
|
+
view.craftRequestedCornerRadii = hasCornerRadii
|
|
4899
|
+
? cornerRadiusKeys.map { number(style[$0]) ?? borderRadius }
|
|
4900
|
+
: nil
|
|
4901
|
+
let borderWidth = number(style["borderWidth"]) ?? 0
|
|
4902
|
+
let borderColor = color(style["borderColor"]) ?? .clear
|
|
4903
|
+
let sideWidthKeys = ["borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth"]
|
|
4904
|
+
let sideColorKeys = ["borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor"]
|
|
4905
|
+
view.craftApplySideBorders(
|
|
4906
|
+
(sideWidthKeys + sideColorKeys).contains { style[$0] != nil },
|
|
4907
|
+
style: style["borderStyle"] as? String,
|
|
4908
|
+
widths: sideWidthKeys.map { number(style[$0]) ?? borderWidth },
|
|
4909
|
+
colors: sideColorKeys.map { color(style[$0]) ?? borderColor }
|
|
4910
|
+
)
|
|
4911
|
+
view.layer.zPosition = number(style["zIndex"]) ?? 0
|
|
4912
|
+
applyTransform(style["transform"], to: view)
|
|
4716
4913
|
let elevation = max(0, number(style["elevation"]) ?? 0)
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4914
|
+
let shadowColor = color(style["shadowColor"]) ?? .black
|
|
4915
|
+
let shadowOpacity = min(1, max(0, number(style["shadowOpacity"]) ?? (elevation > 0 ? min(0.28, 0.12 + elevation * 0.02) : 0)))
|
|
4916
|
+
let shadowRadius = max(0, number(style["shadowRadius"]) ?? elevation * 0.5)
|
|
4917
|
+
let shadowOffset = style["shadowOffset"] as? [String: Any]
|
|
4918
|
+
view.layer.shadowColor = shadowColor.cgColor
|
|
4919
|
+
view.layer.shadowOpacity = Float(shadowOpacity)
|
|
4920
|
+
view.layer.shadowRadius = shadowRadius
|
|
4921
|
+
view.layer.shadowOffset = CGSize(
|
|
4922
|
+
width: number(shadowOffset?["width"]) ?? 0,
|
|
4923
|
+
height: number(shadowOffset?["height"]) ?? (elevation * 0.25)
|
|
4924
|
+
)
|
|
4721
4925
|
// A label paints its background inside its own bounds, so a rounded
|
|
4722
4926
|
// chip of text clips to its corners.
|
|
4723
4927
|
view.clipsToBounds = style["overflow"] as? String == "hidden"
|
|
@@ -4740,6 +4944,49 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4740
4944
|
)
|
|
4741
4945
|
}
|
|
4742
4946
|
|
|
4947
|
+
private func applyTransform(_ raw: Any?, to view: UIView) {
|
|
4948
|
+
view.transform = .identity
|
|
4949
|
+
view.layer.transform = CATransform3DIdentity
|
|
4950
|
+
guard let transforms = raw as? [[String: Any]] else { return }
|
|
4951
|
+
var affine = CGAffineTransform.identity
|
|
4952
|
+
var threeDimensional = CATransform3DIdentity
|
|
4953
|
+
var hasThreeDimensionalRotation = false
|
|
4954
|
+
for transform in transforms {
|
|
4955
|
+
if let value = number(transform["translateX"]) { affine = affine.translatedBy(x: value, y: 0) }
|
|
4956
|
+
if let value = number(transform["translateY"]) { affine = affine.translatedBy(x: 0, y: value) }
|
|
4957
|
+
if let value = number(transform["scale"]) { affine = affine.scaledBy(x: value, y: value) }
|
|
4958
|
+
if let value = number(transform["scaleX"]) { affine = affine.scaledBy(x: value, y: 1) }
|
|
4959
|
+
if let value = number(transform["scaleY"]) { affine = affine.scaledBy(x: 1, y: value) }
|
|
4960
|
+
if let value = transformAngle(transform["rotate"] ?? transform["rotateZ"]) { affine = affine.rotated(by: value) }
|
|
4961
|
+
if let value = transformAngle(transform["skewX"]) { affine = affine.concatenating(CGAffineTransform(a: 1, b: 0, c: tan(value), d: 1, tx: 0, ty: 0)) }
|
|
4962
|
+
if let value = transformAngle(transform["skewY"]) { affine = affine.concatenating(CGAffineTransform(a: 1, b: tan(value), c: 0, d: 1, tx: 0, ty: 0)) }
|
|
4963
|
+
if let value = transformAngle(transform["rotateX"]) {
|
|
4964
|
+
threeDimensional = CATransform3DRotate(threeDimensional, value, 1, 0, 0)
|
|
4965
|
+
hasThreeDimensionalRotation = true
|
|
4966
|
+
}
|
|
4967
|
+
if let value = transformAngle(transform["rotateY"]) {
|
|
4968
|
+
threeDimensional = CATransform3DRotate(threeDimensional, value, 0, 1, 0)
|
|
4969
|
+
hasThreeDimensionalRotation = true
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
if hasThreeDimensionalRotation {
|
|
4973
|
+
view.layer.transform = CATransform3DConcat(CATransform3DMakeAffineTransform(affine), threeDimensional)
|
|
4974
|
+
} else {
|
|
4975
|
+
view.transform = affine
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4979
|
+
private func transformAngle(_ raw: Any?) -> CGFloat? {
|
|
4980
|
+
if let value = number(raw) { return value }
|
|
4981
|
+
guard let text = raw as? String else { return nil }
|
|
4982
|
+
if text.hasSuffix("rad") { return CGFloat(Double(text.dropLast(3)) ?? 0) }
|
|
4983
|
+
if text.hasSuffix("deg") { return CGFloat((Double(text.dropLast(3)) ?? 0) * .pi / 180) }
|
|
4984
|
+
// Keep the conversion explicit: newer Swift toolchains see both the
|
|
4985
|
+
// CGFloat(Double) and CGFloat(Float) initializers through map and
|
|
4986
|
+
// reject the overloaded constructor reference.
|
|
4987
|
+
return Double(text).map { CGFloat($0) }
|
|
4988
|
+
}
|
|
4989
|
+
|
|
4743
4990
|
private func configureText(_ label: UILabel, text: String, style: [String: Any]) {
|
|
4744
4991
|
let transformed = transformedText(text, style: style)
|
|
4745
4992
|
label.textColor = color(style["color"]) ?? .label
|
|
@@ -4747,14 +4994,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4747
4994
|
label.textAlignment = textAlignment(style["textAlign"])
|
|
4748
4995
|
var attributes: [NSAttributedString.Key: Any] = [:]
|
|
4749
4996
|
if let spacing = number(style["letterSpacing"]) { attributes[.kern] = spacing }
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
case "line-through": attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4753
|
-
case "underline line-through":
|
|
4754
|
-
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
4755
|
-
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4756
|
-
default: break
|
|
4757
|
-
}
|
|
4997
|
+
addTextDecorationAttributes(style, to: &attributes)
|
|
4998
|
+
if let shadow = textShadow(style) { attributes[.shadow] = shadow }
|
|
4758
4999
|
if let lineHeight = number(style["lineHeight"]) {
|
|
4759
5000
|
let paragraph = NSMutableParagraphStyle()
|
|
4760
5001
|
paragraph.minimumLineHeight = lineHeight
|
|
@@ -4799,14 +5040,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4799
5040
|
paragraph.maximumLineHeight = lineHeight
|
|
4800
5041
|
attributes[.paragraphStyle] = paragraph
|
|
4801
5042
|
}
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
case "line-through": attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4805
|
-
case "underline line-through":
|
|
4806
|
-
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
4807
|
-
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4808
|
-
default: break
|
|
4809
|
-
}
|
|
5043
|
+
addTextDecorationAttributes(style, to: &attributes)
|
|
5044
|
+
if let shadow = textShadow(style) { attributes[.shadow] = shadow }
|
|
4810
5045
|
if forceUnderline { attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue }
|
|
4811
5046
|
return attributes
|
|
4812
5047
|
}
|
|
@@ -4828,17 +5063,46 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4828
5063
|
paragraph.alignment = alignment
|
|
4829
5064
|
attributes[.paragraphStyle] = paragraph
|
|
4830
5065
|
}
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
case "line-through": attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4834
|
-
case "underline line-through":
|
|
4835
|
-
attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
|
|
4836
|
-
attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
|
|
4837
|
-
default: break
|
|
4838
|
-
}
|
|
5066
|
+
addTextDecorationAttributes(style, to: &attributes)
|
|
5067
|
+
if let shadow = textShadow(style) { attributes[.shadow] = shadow }
|
|
4839
5068
|
return attributes
|
|
4840
5069
|
}
|
|
4841
5070
|
|
|
5071
|
+
private func textShadow(_ style: [String: Any]) -> NSShadow? {
|
|
5072
|
+
guard let shadowColor = color(style["textShadowColor"]) else { return nil }
|
|
5073
|
+
let offset = style["textShadowOffset"] as? [String: Any]
|
|
5074
|
+
let shadow = NSShadow()
|
|
5075
|
+
shadow.shadowColor = shadowColor
|
|
5076
|
+
shadow.shadowOffset = CGSize(
|
|
5077
|
+
width: number(offset?["width"]) ?? 0,
|
|
5078
|
+
height: number(offset?["height"]) ?? 0
|
|
5079
|
+
)
|
|
5080
|
+
shadow.shadowBlurRadius = max(0, number(style["textShadowRadius"]) ?? 0)
|
|
5081
|
+
return shadow
|
|
5082
|
+
}
|
|
5083
|
+
|
|
5084
|
+
private func addTextDecorationAttributes(_ style: [String: Any], to attributes: inout [NSAttributedString.Key: Any]) {
|
|
5085
|
+
let line = style["textDecorationLine"] as? String
|
|
5086
|
+
let decorationStyle: NSUnderlineStyle = {
|
|
5087
|
+
switch style["textDecorationStyle"] as? String {
|
|
5088
|
+
case "double": return .double
|
|
5089
|
+
case "dotted": return [.single, .patternDot]
|
|
5090
|
+
case "dashed": return [.single, .patternDash]
|
|
5091
|
+
default: return .single
|
|
5092
|
+
}
|
|
5093
|
+
}()
|
|
5094
|
+
let rawStyle = decorationStyle.rawValue
|
|
5095
|
+
let decorationColor = color(style["textDecorationColor"])
|
|
5096
|
+
if line == "underline" || line == "underline line-through" {
|
|
5097
|
+
attributes[.underlineStyle] = rawStyle
|
|
5098
|
+
if let decorationColor { attributes[.underlineColor] = decorationColor }
|
|
5099
|
+
}
|
|
5100
|
+
if line == "line-through" || line == "underline line-through" {
|
|
5101
|
+
attributes[.strikethroughStyle] = rawStyle
|
|
5102
|
+
if let decorationColor { attributes[.strikethroughColor] = decorationColor }
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
|
|
4842
5106
|
private func transformedText(_ text: String, style: [String: Any]) -> String {
|
|
4843
5107
|
switch style["textTransform"] as? String {
|
|
4844
5108
|
case "uppercase": return text.uppercased()
|
|
@@ -5237,11 +5501,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5237
5501
|
if let number = value as? NSNumber, !(value is Bool) { return min(maximum, max(0, number.doubleValue)) }
|
|
5238
5502
|
guard let text = (value as? String)?.trimmingCharacters(in: .whitespaces), !text.isEmpty else { return nil }
|
|
5239
5503
|
if let seconds = Double(text), seconds.isFinite { return min(maximum, max(0, seconds)) }
|
|
5240
|
-
let parts = text.split(separator: ":")
|
|
5241
|
-
guard parts.count == 2 || parts.count == 3
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
let seconds = parts.count == 3 ? parts[2] : 0
|
|
5504
|
+
let parts = text.split(separator: ":")
|
|
5505
|
+
guard parts.count == 2 || parts.count == 3,
|
|
5506
|
+
let hours = Double(parts[0]),
|
|
5507
|
+
let minutes = Double(parts[1]) else { return nil }
|
|
5508
|
+
let seconds = parts.count == 3 ? (Double(parts[2]) ?? -1) : 0
|
|
5245
5509
|
guard hours >= 0, minutes >= 0, minutes < 60, seconds >= 0, seconds < 60 else { return nil }
|
|
5246
5510
|
return min(maximum, hours * 3600 + minutes * 60 + seconds)
|
|
5247
5511
|
}
|
package/dist/mobile.js
CHANGED
|
@@ -1369,6 +1369,12 @@ var pushNotifications = {
|
|
|
1369
1369
|
return onCraftEvent("craftNotificationReceived", (detail) => callback(detail));
|
|
1370
1370
|
}
|
|
1371
1371
|
};
|
|
1372
|
+
function healthHeartRateSamples(detail) {
|
|
1373
|
+
const samples = detail?.samples;
|
|
1374
|
+
if (!Array.isArray(samples))
|
|
1375
|
+
return [];
|
|
1376
|
+
return samples.filter((sample) => typeof sample?.bpm === "number" && Number.isFinite(sample.bpm) && typeof sample.startDate === "number" && typeof sample.endDate === "number");
|
|
1377
|
+
}
|
|
1372
1378
|
var health = {
|
|
1373
1379
|
supports(method) {
|
|
1374
1380
|
return typeof getCraftRoot()?.health?.[method] === "function";
|
|
@@ -1432,6 +1438,29 @@ var health = {
|
|
|
1432
1438
|
if (!craft?.health?.getMindfulSessions)
|
|
1433
1439
|
throw new Error("Reading mindful sessions is unavailable");
|
|
1434
1440
|
return craft.health.getMindfulSessions(options);
|
|
1441
|
+
},
|
|
1442
|
+
async startHeartRateUpdates(options = {}) {
|
|
1443
|
+
const craft = getCraftRoot();
|
|
1444
|
+
if (!craft?.health?.startHeartRateUpdates)
|
|
1445
|
+
throw new Error("Heart-rate updates are unavailable");
|
|
1446
|
+
return Boolean(await craft.health.startHeartRateUpdates(options));
|
|
1447
|
+
},
|
|
1448
|
+
async stopHeartRateUpdates() {
|
|
1449
|
+
const craft = getCraftRoot();
|
|
1450
|
+
if (!craft?.health?.stopHeartRateUpdates)
|
|
1451
|
+
throw new Error("Heart-rate updates are unavailable");
|
|
1452
|
+
return Boolean(await craft.health.stopHeartRateUpdates());
|
|
1453
|
+
},
|
|
1454
|
+
onHeartRate(callback) {
|
|
1455
|
+
if (typeof window === "undefined" || typeof getCraftRoot()?.health?.startHeartRateUpdates !== "function")
|
|
1456
|
+
return () => {};
|
|
1457
|
+
const listener = (event) => {
|
|
1458
|
+
const samples = healthHeartRateSamples(event.detail);
|
|
1459
|
+
if (samples.length)
|
|
1460
|
+
callback(samples);
|
|
1461
|
+
};
|
|
1462
|
+
window.addEventListener("craftHealthHeartRate", listener);
|
|
1463
|
+
return () => window.removeEventListener("craftHealthHeartRate", listener);
|
|
1435
1464
|
}
|
|
1436
1465
|
};
|
|
1437
1466
|
async function updateLiveActivity(idOrState, state) {
|