craft-native 0.0.243 → 0.0.244
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/src/index.js +3 -1
- package/dist/android/templates/CraftNativeMutation.kt.template +7 -3
- package/dist/android/templates/MainActivityNative.kt.template +17 -8
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +9 -1
- package/dist/ios/templates/CraftNativeMutation.swift +4 -0
- package/dist/ios/templates/CraftNativeScreen.swift +46 -17
- package/dist/ios/templates/testing/DriverTests.swift.template +5 -1
- package/dist/macos/templates/CraftNativeMutation.swift +4 -0
- package/package.json +1 -1
|
@@ -323,7 +323,9 @@ async function createAndroidEmulator(options) {
|
|
|
323
323
|
const packages = await run([adb, "-s", serial, "shell", "pm", "path", "android"], undefined, probeTimeout(), true);
|
|
324
324
|
const systemUi = await run([adb, "-s", serial, "shell", "pidof", "com.android.systemui"], undefined, probeTimeout(), true);
|
|
325
325
|
const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-t", "1"], undefined, probeTimeout(), true);
|
|
326
|
-
|
|
326
|
+
const home = await run([adb, "-s", serial, "shell", "cmd", "package", "resolve-activity", "--brief", "-a", "android.intent.action.MAIN", "-c", "android.intent.category.HOME"], undefined, probeTimeout(), true);
|
|
327
|
+
const homeActivity = home.stdout.trim().split(/\r?\n/).at(-1) ?? "";
|
|
328
|
+
booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim()) && home.code === 0 && /^[\w.]+\/[\w.$]+$/.test(homeActivity) && !homeActivity.startsWith("com.android.sdksetup/");
|
|
327
329
|
}
|
|
328
330
|
}
|
|
329
331
|
const checkedAt = Date.now();
|
|
@@ -30,7 +30,8 @@ class CraftNativeMutationDocument {
|
|
|
30
30
|
var events: JSONObject,
|
|
31
31
|
var text: MutableList<String>,
|
|
32
32
|
var children: MutableList<String>,
|
|
33
|
-
var parent: String
|
|
33
|
+
var parent: String?,
|
|
34
|
+
val key: String? = null
|
|
34
35
|
) {
|
|
35
36
|
fun duplicate(): Node = copy(
|
|
36
37
|
props = JSONObject(props.toString()),
|
|
@@ -175,7 +176,8 @@ class CraftNativeMutationDocument {
|
|
|
175
176
|
dictionary(value.opt("events")),
|
|
176
177
|
textChildren(if (value.has("children")) value.opt("children") else value.opt("text")),
|
|
177
178
|
mutableListOf(),
|
|
178
|
-
null
|
|
179
|
+
null,
|
|
180
|
+
key = value.opt("key") as? String
|
|
179
181
|
)
|
|
180
182
|
return if (makeRoot) id else root
|
|
181
183
|
}
|
|
@@ -299,6 +301,7 @@ class CraftNativeMutationDocument {
|
|
|
299
301
|
.put("style", node.style)
|
|
300
302
|
.put("events", node.events)
|
|
301
303
|
.put("children", children)
|
|
304
|
+
.also { value -> node.key?.let { value.put("key", it) } }
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
private fun importNode(
|
|
@@ -322,7 +325,8 @@ class CraftNativeMutationDocument {
|
|
|
322
325
|
dictionary(value.opt("events")),
|
|
323
326
|
mutableListOf(),
|
|
324
327
|
mutableListOf(),
|
|
325
|
-
parent
|
|
328
|
+
parent,
|
|
329
|
+
key = value.opt("key") as? String
|
|
326
330
|
)
|
|
327
331
|
nodes[id] = node
|
|
328
332
|
for (index in 0 until children.length()) {
|
|
@@ -4937,8 +4937,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4937
4937
|
val explicitKey = authoredKey(node, props)
|
|
4938
4938
|
// Descendants are scoped to this retained identity, not its sibling
|
|
4939
4939
|
// position. Moving a keyed parent must retain its nested inputs too.
|
|
4940
|
+
// A test selector may intentionally identify several list rows. The
|
|
4941
|
+
// compiler's unique protocol ID must win over that legacy fallback.
|
|
4940
4942
|
val identity = explicitKey?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
4941
4943
|
?: protocolId?.let { "id:$it" }
|
|
4944
|
+
?: props.optString("testID").takeIf { it.isNotBlank() }?.let { "${path.substringBeforeLast('.', "")}/$it" }
|
|
4942
4945
|
?: path
|
|
4943
4946
|
val previous = previousControls[identity]
|
|
4944
4947
|
val reusableInput = previous as? EditText
|
|
@@ -5824,7 +5827,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5824
5827
|
}
|
|
5825
5828
|
|
|
5826
5829
|
private fun authoredKey(node: JSONObject, props: JSONObject): String? {
|
|
5827
|
-
val direct = listOf(node.optString("key"), props.optString("key")
|
|
5830
|
+
val direct = listOf(node.optString("key"), props.optString("key"))
|
|
5828
5831
|
.firstOrNull { it.isNotBlank() }
|
|
5829
5832
|
if (direct != null) return direct
|
|
5830
5833
|
return null
|
|
@@ -5880,17 +5883,23 @@ class MainActivity : AppCompatActivity() {
|
|
|
5880
5883
|
}
|
|
5881
5884
|
if (craftNativeOptionalString(style.opt("flexDirection"))?.endsWith("-reverse") == true) desired.reverse()
|
|
5882
5885
|
val desiredSet = desired.toSet()
|
|
5883
|
-
for (
|
|
5884
|
-
if (layout.getChildAt(index)
|
|
5885
|
-
|
|
5886
|
+
for (index in layout.childCount - 1 downTo 0) {
|
|
5887
|
+
if (layout.getChildAt(index) !in desiredSet) layout.removeViewAt(index)
|
|
5888
|
+
}
|
|
5889
|
+
for (childView in desired) {
|
|
5890
|
+
if (childView.parent !== layout) {
|
|
5886
5891
|
(childView.parent as? ViewGroup)?.removeView(childView)
|
|
5887
|
-
layout.addView(childView
|
|
5892
|
+
layout.addView(childView)
|
|
5888
5893
|
}
|
|
5889
5894
|
val params = childView.layoutParams as? LinearLayout.LayoutParams
|
|
5890
5895
|
?: LinearLayout.LayoutParams(childView.layoutParams?.width ?: -1, childView.layoutParams?.height ?: -2)
|
|
5891
5896
|
childView.layoutParams = params
|
|
5892
5897
|
}
|
|
5893
|
-
|
|
5898
|
+
// Reorder attached children without removing their focused inputs.
|
|
5899
|
+
// Removing and adding an existing row clears Android's native focus.
|
|
5900
|
+
if (desired.indices.any { layout.getChildAt(it) !== desired[it] }) {
|
|
5901
|
+
desired.forEach(layout::bringChildToFront)
|
|
5902
|
+
}
|
|
5894
5903
|
layout.requestLayout()
|
|
5895
5904
|
}
|
|
5896
5905
|
|
|
@@ -6573,11 +6582,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
6573
6582
|
}
|
|
6574
6583
|
|
|
6575
6584
|
private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) {
|
|
6576
|
-
|
|
6585
|
+
val testId = craftNativeOptionalString(props.opt("testID"))
|
|
6586
|
+
view.tag = testId?.takeIf { it.isNotBlank() } ?: fallbackLabel
|
|
6577
6587
|
val label = craftNativeOptionalString(props.opt("accessibilityLabel"))
|
|
6578
6588
|
?: craftNativeOptionalString(props.opt("alt"))?.takeIf { type == "Image" || type == "ImageBackground" }
|
|
6579
6589
|
val value = craftNativeOptionalString(props.opt("accessibilityValue"))
|
|
6580
|
-
val testId = craftNativeOptionalString(props.opt("testID"))
|
|
6581
6590
|
view.contentDescription = when {
|
|
6582
6591
|
label != null && value != null -> "$label, $value"
|
|
6583
6592
|
label != null -> label
|
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.244";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -670,6 +670,7 @@ final class CraftChrome: ObservableObject {
|
|
|
670
670
|
|
|
671
671
|
@Published var tabs: [Tab] = []
|
|
672
672
|
@Published var tabAccessory: Tab?
|
|
673
|
+
private weak var tabAccessoryOwner: AnyObject?
|
|
673
674
|
@Published var selected: String?
|
|
674
675
|
let tabSwipeProgress = CraftTabSwipeProgress()
|
|
675
676
|
private(set) var tabSwipePosition: CGFloat? {
|
|
@@ -771,7 +772,7 @@ final class CraftChrome: ObservableObject {
|
|
|
771
772
|
tabs[index] = next
|
|
772
773
|
}
|
|
773
774
|
/// A destination beside the tab pill, outside the tab stacks and swipe order.
|
|
774
|
-
func updateTabAccessory(_ item: [String: Any]?) {
|
|
775
|
+
func updateTabAccessory(_ item: [String: Any]?, owner: AnyObject? = nil) {
|
|
775
776
|
let next: Tab?
|
|
776
777
|
if let item, let path = item["path"] as? String,
|
|
777
778
|
path.hasPrefix("/"), !path.hasPrefix("//"),
|
|
@@ -782,10 +783,17 @@ final class CraftChrome: ObservableObject {
|
|
|
782
783
|
} else {
|
|
783
784
|
next = nil
|
|
784
785
|
}
|
|
786
|
+
tabAccessoryOwner = owner
|
|
785
787
|
guard tabAccessory != next else { return }
|
|
786
788
|
tabAccessory = next
|
|
787
789
|
}
|
|
788
790
|
|
|
791
|
+
/// A retained or prepared screen cannot clear another screen's chrome.
|
|
792
|
+
func clearTabAccessory(ownedBy owner: AnyObject) {
|
|
793
|
+
guard tabAccessoryOwner === owner else { return }
|
|
794
|
+
updateTabAccessory(nil)
|
|
795
|
+
}
|
|
796
|
+
|
|
789
797
|
func setHiddenByScreen(_ hidden: Bool) {
|
|
790
798
|
guard hiddenByScreen != hidden else { return }
|
|
791
799
|
hiddenByScreen = hidden
|
|
@@ -29,6 +29,7 @@ struct CraftNativeMutationResult {
|
|
|
29
29
|
final class CraftNativeMutationDocument {
|
|
30
30
|
private struct Node {
|
|
31
31
|
let id: String
|
|
32
|
+
let key: String?
|
|
32
33
|
var type: String
|
|
33
34
|
var props: [String: Any]
|
|
34
35
|
var style: [String: Any]
|
|
@@ -177,6 +178,7 @@ final class CraftNativeMutationDocument {
|
|
|
177
178
|
}
|
|
178
179
|
nodes[id] = Node(
|
|
179
180
|
id: id,
|
|
181
|
+
key: value["key"] as? String,
|
|
180
182
|
type: type,
|
|
181
183
|
props: dictionary(value["props"]),
|
|
182
184
|
style: dictionary(value["style"]),
|
|
@@ -330,6 +332,7 @@ final class CraftNativeMutationDocument {
|
|
|
330
332
|
"style": node.style,
|
|
331
333
|
"events": node.events,
|
|
332
334
|
]
|
|
335
|
+
if let key = node.key { value["key"] = key }
|
|
333
336
|
let text: [Any] = node.text
|
|
334
337
|
let children: [Any] = node.children.compactMap { materialize($0, nodes: nodes) }
|
|
335
338
|
value["children"] = text + children
|
|
@@ -350,6 +353,7 @@ final class CraftNativeMutationDocument {
|
|
|
350
353
|
let children = value["children"] as? [Any] ?? []
|
|
351
354
|
nodes[id] = Node(
|
|
352
355
|
id: id,
|
|
356
|
+
key: value["key"] as? String,
|
|
353
357
|
type: value["type"] as? String ?? "View",
|
|
354
358
|
props: props,
|
|
355
359
|
style: dictionary(value["style"]),
|
|
@@ -1435,21 +1435,47 @@ private final class CraftNativeMapView: MKMapView, MKMapViewDelegate {
|
|
|
1435
1435
|
|
|
1436
1436
|
private func configureMarkerView(_ view: MKAnnotationView, id: String) {
|
|
1437
1437
|
if let bearing = markerBearings[id] {
|
|
1438
|
-
let
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
// the contrasting route arrow when displayed over map content.
|
|
1442
|
-
view.image = UIImage(systemName: "arrow.up.circle.fill", withConfiguration: configuration)?
|
|
1443
|
-
.withRenderingMode(.alwaysOriginal)
|
|
1438
|
+
let color = (markerPinColors[id] ?? .systemGreen).resolvedColor(with: traitCollection)
|
|
1439
|
+
view.tintColor = color
|
|
1440
|
+
view.image = Self.markerImage(color: color, direction: true)
|
|
1444
1441
|
view.transform = CGAffineTransform(rotationAngle: CGFloat((bearing - camera.heading) * .pi / 180))
|
|
1445
1442
|
} else {
|
|
1446
|
-
|
|
1447
|
-
view.
|
|
1448
|
-
|
|
1443
|
+
let color = (markerPinColors[id] ?? .systemRed).resolvedColor(with: traitCollection)
|
|
1444
|
+
view.tintColor = color
|
|
1445
|
+
view.image = Self.markerImage(color: color, direction: false)
|
|
1449
1446
|
view.transform = .identity
|
|
1450
1447
|
}
|
|
1451
1448
|
}
|
|
1452
1449
|
|
|
1450
|
+
private static let markerImages: NSCache<NSString, UIImage> = {
|
|
1451
|
+
let cache = NSCache<NSString, UIImage>()
|
|
1452
|
+
cache.countLimit = 64
|
|
1453
|
+
return cache
|
|
1454
|
+
}()
|
|
1455
|
+
|
|
1456
|
+
private static func markerImage(color: UIColor, direction: Bool) -> UIImage {
|
|
1457
|
+
let key = "\(direction ? "direction" : "pin"):\(color.description)" as NSString
|
|
1458
|
+
if let cached = markerImages.object(forKey: key) { return cached }
|
|
1459
|
+
let size = direction ? CGSize(width: 20, height: 24) : CGSize(width: 28, height: 28)
|
|
1460
|
+
// Explicit native drawing preserves the light backing independently
|
|
1461
|
+
// of MapKit's tint and SF Symbols' multicolor rendering preferences.
|
|
1462
|
+
let image = UIGraphicsImageRenderer(size: size).image { _ in
|
|
1463
|
+
let border = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size).insetBy(dx: 1, dy: 1),
|
|
1464
|
+
cornerRadius: (size.width - 2) / 2)
|
|
1465
|
+
UIColor.white.setFill()
|
|
1466
|
+
border.fill()
|
|
1467
|
+
color.setStroke()
|
|
1468
|
+
border.lineWidth = 1.5
|
|
1469
|
+
border.stroke()
|
|
1470
|
+
let symbol = UIImage(systemName: direction ? "arrow.up" : "mappin",
|
|
1471
|
+
withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold))?
|
|
1472
|
+
.withTintColor(color, renderingMode: .alwaysOriginal)
|
|
1473
|
+
symbol?.draw(in: direction ? CGRect(x: 5, y: 6, width: 10, height: 12) : CGRect(x: 7, y: 5, width: 14, height: 18))
|
|
1474
|
+
}.withRenderingMode(.alwaysOriginal)
|
|
1475
|
+
markerImages.setObject(image, forKey: key)
|
|
1476
|
+
return image
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1453
1479
|
override func layoutSubviews() {
|
|
1454
1480
|
super.layoutSubviews()
|
|
1455
1481
|
if tileTemplate != nil {
|
|
@@ -4621,10 +4647,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4621
4647
|
super.viewWillDisappear(animated)
|
|
4622
4648
|
screenVisibilityChanged(false)
|
|
4623
4649
|
stopCelebrations(in: view)
|
|
4624
|
-
//
|
|
4625
|
-
//
|
|
4626
|
-
|
|
4627
|
-
CraftChrome.shared.updateTabAccessory(nil)
|
|
4650
|
+
// Release only this screen's accessory. A retained or prepared screen
|
|
4651
|
+
// can disappear after the destination has already published its own.
|
|
4652
|
+
CraftChrome.shared.clearTabAccessory(ownedBy: self)
|
|
4628
4653
|
// Popped (or its stack dismissed) while the bar was away: it comes
|
|
4629
4654
|
// back for whatever shows next, a web page included.
|
|
4630
4655
|
if tabBarHidden, isMovingFromParent || isBeingDismissed || navigationController?.isBeingDismissed == true {
|
|
@@ -4669,7 +4694,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4669
4694
|
private func restoreShellChrome() {
|
|
4670
4695
|
guard !isAppScript else { return }
|
|
4671
4696
|
CraftChrome.shared.setHiddenByScreen(tabBarHidden)
|
|
4672
|
-
CraftChrome.shared.updateTabAccessory(tabAccessory)
|
|
4697
|
+
CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
|
|
4673
4698
|
}
|
|
4674
4699
|
|
|
4675
4700
|
/// The screen came on screen or went off it: a screen pushed over it was
|
|
@@ -5340,7 +5365,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5340
5365
|
if options.keys.contains("tabAccessory") {
|
|
5341
5366
|
tabAccessory = options["tabAccessory"] as? [String: Any]
|
|
5342
5367
|
if navigationController?.topViewController === self, view.window != nil {
|
|
5343
|
-
CraftChrome.shared.updateTabAccessory(tabAccessory)
|
|
5368
|
+
CraftChrome.shared.updateTabAccessory(tabAccessory, owner: self)
|
|
5344
5369
|
}
|
|
5345
5370
|
}
|
|
5346
5371
|
if options.keys.contains("title") {
|
|
@@ -6865,7 +6890,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6865
6890
|
|
|
6866
6891
|
private func explicitKey(_ node: [String: Any], props: [String: Any]) -> String? {
|
|
6867
6892
|
if let authored = authoredKey(node, props: props) { return authored }
|
|
6868
|
-
|
|
6893
|
+
// Repeated test selectors are valid for list rows. Retain each row by
|
|
6894
|
+
// its unique protocol ID before considering legacy selector identity.
|
|
6895
|
+
return [node["id"], props["testID"]]
|
|
6896
|
+
.compactMap { $0 as? String }
|
|
6897
|
+
.first { !$0.isEmpty }
|
|
6869
6898
|
}
|
|
6870
6899
|
|
|
6871
6900
|
/// The key a node's author gave it. A wrapper of one child (a
|
|
@@ -6877,7 +6906,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6877
6906
|
private func authoredKey(_ node: [String: Any], props: [String: Any]) -> String? {
|
|
6878
6907
|
// The current @stacksjs/stx native compiler emits key in props; accept the IR
|
|
6879
6908
|
// field too so keyed children keep working when the compiler adopts it.
|
|
6880
|
-
let direct = [node["key"], props["key"]
|
|
6909
|
+
let direct = [node["key"], props["key"]]
|
|
6881
6910
|
.compactMap { $0 as? String }
|
|
6882
6911
|
.first { !$0.isEmpty }
|
|
6883
6912
|
if let direct { return direct }
|
|
@@ -448,7 +448,11 @@ final class DriverTests: XCTestCase {
|
|
|
448
448
|
var found = element(containing: arg)
|
|
449
449
|
var swipes = 0
|
|
450
450
|
while (found == nil || !(found!.isHittable)) && swipes < 8 {
|
|
451
|
-
app.
|
|
451
|
+
if let target = found, !target.frame.isEmpty, target.frame.midY < app.frame.midY {
|
|
452
|
+
app.swipeDown()
|
|
453
|
+
} else {
|
|
454
|
+
app.swipeUp()
|
|
455
|
+
}
|
|
452
456
|
swipes += 1
|
|
453
457
|
found = element(containing: arg)
|
|
454
458
|
}
|
|
@@ -29,6 +29,7 @@ struct CraftNativeMutationResult {
|
|
|
29
29
|
final class CraftNativeMutationDocument {
|
|
30
30
|
private struct Node {
|
|
31
31
|
let id: String
|
|
32
|
+
let key: String?
|
|
32
33
|
var type: String
|
|
33
34
|
var props: [String: Any]
|
|
34
35
|
var style: [String: Any]
|
|
@@ -177,6 +178,7 @@ final class CraftNativeMutationDocument {
|
|
|
177
178
|
}
|
|
178
179
|
nodes[id] = Node(
|
|
179
180
|
id: id,
|
|
181
|
+
key: value["key"] as? String,
|
|
180
182
|
type: type,
|
|
181
183
|
props: dictionary(value["props"]),
|
|
182
184
|
style: dictionary(value["style"]),
|
|
@@ -330,6 +332,7 @@ final class CraftNativeMutationDocument {
|
|
|
330
332
|
"style": node.style,
|
|
331
333
|
"events": node.events,
|
|
332
334
|
]
|
|
335
|
+
if let key = node.key { value["key"] = key }
|
|
333
336
|
let text: [Any] = node.text
|
|
334
337
|
let children: [Any] = node.children.compactMap { materialize($0, nodes: nodes) }
|
|
335
338
|
value["children"] = text + children
|
|
@@ -350,6 +353,7 @@ final class CraftNativeMutationDocument {
|
|
|
350
353
|
let children = value["children"] as? [Any] ?? []
|
|
351
354
|
nodes[id] = Node(
|
|
352
355
|
id: id,
|
|
356
|
+
key: value["key"] as? String,
|
|
353
357
|
type: value["type"] as? String ?? "View",
|
|
354
358
|
props: props,
|
|
355
359
|
style: dictionary(value["style"]),
|