craft-native 0.0.247 → 0.0.248

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/android/src/index.d.ts +2 -0
  2. package/dist/android/src/index.js +22 -18
  3. package/dist/android/templates/CraftNative.kt.template +17 -0
  4. package/dist/android/templates/CraftNativeFlatList.kt.template +115 -40
  5. package/dist/android/templates/CraftNativeMapGoogle.kt.template +92 -5
  6. package/dist/android/templates/CraftNativeMapLibre.kt.template +90 -3
  7. package/dist/android/templates/CraftNativeMutation.kt.template +32 -0
  8. package/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +7 -2
  9. package/dist/android/templates/CraftNativeServices.kt.template +25 -0
  10. package/dist/android/templates/CraftNativeShell.kt.template +34 -8
  11. package/dist/android/templates/MainActivityNative.kt.template +369 -69
  12. package/dist/android/templates/build.gradle.kts.project.template +1 -1
  13. package/dist/api/mobile.d.ts +37 -1
  14. package/dist/browser.js +9 -9
  15. package/dist/cli.js +90 -673
  16. package/dist/components/index.d.ts +50 -9
  17. package/dist/index.cjs +394 -438
  18. package/dist/index.d.cts +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +446 -493
  21. package/dist/ios/src/index.d.ts +2 -0
  22. package/dist/ios/src/index.js +54 -53
  23. package/dist/ios/templates/CraftApp.swift +37 -2
  24. package/dist/ios/templates/CraftHealth.swift +217 -0
  25. package/dist/ios/templates/CraftHeartRate.swift +6 -3
  26. package/dist/ios/templates/CraftHybrid.swift +18 -19
  27. package/dist/ios/templates/CraftLiveActivities.swift +1 -1
  28. package/dist/ios/templates/CraftNativeActions.swift +87 -8
  29. package/dist/ios/templates/CraftNativeFiles.swift +15 -3
  30. package/dist/ios/templates/CraftNativeFlatList.swift +18 -1
  31. package/dist/ios/templates/CraftNativeMutation.swift +36 -0
  32. package/dist/ios/templates/CraftNativeScreen.swift +813 -65
  33. package/dist/ios/templates/CraftNativeServices.swift +4 -2
  34. package/dist/ios/templates/CraftNativeUI.swift +10 -2
  35. package/dist/ios/templates/testing/DriverTests.swift.template +1 -0
  36. package/dist/ios/templates/testing/HostApp.swift +9 -2
  37. package/dist/macos/templates/CraftNativeMutation.swift +36 -0
  38. package/dist/macos/templates/native_layout.zig +226 -2
  39. package/dist/mobile-testing.js +2 -2
  40. package/dist/mobile.js +101 -63
  41. package/package.json +1 -1
@@ -57,11 +57,24 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
57
57
  rightMargin = (8 * resources.displayMetrics.density).roundToInt()
58
58
  bottomMargin = (8 * resources.displayMetrics.density).roundToInt()
59
59
  })
60
+ refreshThemeDefaults()
60
61
  mapView.onCreate(null)
61
62
  mapView.addOnDidFinishLoadingMapListener { if (!destroyed && initialCameraKey == null) applyPending() }
62
63
  mapView.getMapAsync(this)
63
64
  }
64
65
 
66
+ fun refreshThemeDefaults() {
67
+ val dark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
68
+ val foreground = if (dark) Color.rgb(245, 245, 245) else Color.rgb(37, 51, 33)
69
+ val surface = if (dark) Color.argb(230, 38, 38, 38) else Color.argb(230, 255, 255, 255)
70
+ attribution.setTextColor(foreground)
71
+ attribution.setBackgroundColor(surface)
72
+ scale.setBackgroundColor(surface)
73
+ scaleLabel.setTextColor(foreground)
74
+ scaleLine.setBackgroundColor(foreground)
75
+ readyStyle?.let { updateOverlays(it) }
76
+ }
77
+
65
78
  override fun onMapReady(nativeMap: MapLibreMap) {
66
79
  if (destroyed) return
67
80
  map = nativeMap
@@ -109,6 +122,14 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
109
122
  fun markerHandler(id: String): String? = markerHandlers[id]
110
123
 
111
124
  private fun applyPending() {
125
+ // MapLibre may deliver style and render callbacks from its worker
126
+ // thread. All map, style, and GeoJSON source mutations must happen on
127
+ // Android's UI thread, so marshal the whole reconciliation before
128
+ // touching the native map object.
129
+ if (Looper.myLooper() != Looper.getMainLooper()) {
130
+ post { applyPending() }
131
+ return
132
+ }
112
133
  val nativeMap = map ?: return
113
134
  if (destroyed) return
114
135
  nativeMap.uiSettings.isCompassEnabled = craftNativeBoolean(pendingProps.opt("showsCompass"))
@@ -200,6 +221,12 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
200
221
  style.addLayer(LineLayer("craft-map-routes", "craft-map-overlays")
201
222
  .withFilter(Expression.eq(Expression.get("kind"), "line"))
202
223
  .withProperties(lineColor(Expression.toColor(Expression.get("color"))), lineWidth(Expression.get("width")), lineCap("round"), lineJoin("round")))
224
+ style.addLayer(FillLayer("craft-map-polygons", "craft-map-overlays")
225
+ .withFilter(Expression.eq(Expression.get("kind"), "polygon"))
226
+ .withProperties(fillColor(Expression.toColor(Expression.get("fillColor")))))
227
+ style.addLayer(LineLayer("craft-map-polygon-strokes", "craft-map-overlays")
228
+ .withFilter(Expression.eq(Expression.get("kind"), "polygon"))
229
+ .withProperties(lineColor(Expression.toColor(Expression.get("strokeColor"))), lineWidth(Expression.get("strokeWidth")), lineCap("round"), lineJoin("round")))
203
230
  style.addLayer(CircleLayer("craft-map-markers", "craft-map-overlays")
204
231
  .withFilter(Expression.eq(Expression.get("kind"), "marker"))
205
232
  .withProperties(circleColor(Expression.toColor(Expression.get("color"))), circleRadius(7f), circleStrokeColor(Color.WHITE), circleStrokeWidth(2f)))
@@ -212,6 +239,42 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
212
239
  val features = JSONArray()
213
240
  val pointsForFit = mutableListOf<LatLng>()
214
241
  markerHandlers.clear()
242
+ val dataMarkers = pendingProps.optJSONArray("markers")
243
+ for (index in 0 until (dataMarkers?.length() ?: 0)) {
244
+ val overlay = dataMarkers?.optJSONObject(index) ?: continue
245
+ val id = craftNativeOptionalString(overlay.opt("id")) ?: continue
246
+ val point = coordinates(overlay.optJSONArray("geometry")).firstOrNull() ?: continue
247
+ val bearing = finiteNumber(overlay.opt("bearing"))
248
+ val properties = JSONObject()
249
+ .put("craftOverlayId", id)
250
+ .put("kind", if (bearing == null) "marker" else "direction")
251
+ .put("craftMarkerId", id)
252
+ .put("bearing", (bearing ?: 0.0) % 360)
253
+ .put("color", cssColor(overlay.opt("pinColor"), Color.rgb(36, 114, 75)))
254
+ val geometry = JSONObject().put("type", "Point").put("coordinates", JSONArray().put(point.longitude).put(point.latitude))
255
+ features.put(JSONObject().put("type", "Feature").put("properties", properties).put("geometry", geometry))
256
+ pointsForFit.add(point)
257
+ }
258
+ val dataPolylines = pendingProps.optJSONArray("polylines")
259
+ for (index in 0 until (dataPolylines?.length() ?: 0)) {
260
+ val overlay = dataPolylines?.optJSONObject(index) ?: continue
261
+ val id = craftNativeOptionalString(overlay.opt("id")) ?: continue
262
+ val points = coordinates(overlay.optJSONArray("geometry"))
263
+ if (points.size <= 1) continue
264
+ val rawWidth = overlay.opt("strokeWidth")
265
+ val logicalWidth = if (rawWidth == null || rawWidth == JSONObject.NULL) 3.0 else finiteNumber(rawWidth) ?: continue
266
+ val width = (logicalWidth * resources.displayMetrics.density).takeIf { it.isFinite() && it > 0 } ?: continue
267
+ val properties = JSONObject()
268
+ .put("craftOverlayId", id)
269
+ .put("kind", "line")
270
+ .put("color", cssColor(overlay.opt("strokeColor"), Color.BLUE))
271
+ .put("width", width)
272
+ val geometry = JSONObject().put("type", "LineString").put("coordinates", JSONArray().apply {
273
+ points.forEach { put(JSONArray().put(it.longitude).put(it.latitude)) }
274
+ })
275
+ features.put(JSONObject().put("type", "Feature").put("properties", properties).put("geometry", geometry))
276
+ pointsForFit.addAll(points)
277
+ }
215
278
  for (index in 0 until pendingChildren.length()) {
216
279
  val child = pendingChildren.optJSONObject(index) ?: continue
217
280
  val type = child.optString("type").lowercase()
@@ -231,11 +294,25 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
231
294
  pointsForFit.add(point)
232
295
  } else if ((type == "mappolyline" || type == "polyline") && points.size > 1) {
233
296
  val authoredWidth = props.opt("strokeWidth")
234
- val width = if (authoredWidth == null || authoredWidth == JSONObject.NULL) 3.0 else finiteNumber(authoredWidth) ?: continue
235
- if (width <= 0 || !width.toFloat().isFinite()) continue
297
+ val logicalWidth = if (authoredWidth == null || authoredWidth == JSONObject.NULL) 3.0 else finiteNumber(authoredWidth) ?: continue
298
+ // The shared contract is expressed in logical points. MapLibre's
299
+ // style line-width is in physical pixels, just like the Google
300
+ // Maps polyline width, so convert only at this host boundary.
301
+ val width = (logicalWidth * resources.displayMetrics.density)
302
+ .takeIf { it.isFinite() && it > 0 } ?: continue
236
303
  properties.put("kind", "line").put("color", cssColor(props.opt("strokeColor"), Color.BLUE)).put("width", width)
237
304
  geometry.put("type", "LineString").put("coordinates", JSONArray().apply { points.forEach { put(JSONArray().put(it.longitude).put(it.latitude)) } })
238
305
  pointsForFit.addAll(points)
306
+ } else if ((type == "mappolygon" || type == "polygon") && points.size > 2) {
307
+ val rawWidth = props.opt("strokeWidth")
308
+ val logicalWidth = if (rawWidth == null || rawWidth == JSONObject.NULL) 2.0 else finiteNumber(rawWidth) ?: continue
309
+ val width = (logicalWidth * resources.displayMetrics.density).takeIf { it.isFinite() && it > 0 } ?: continue
310
+ val fill = cssColor(props.opt("fillColor"), Color.argb(72, 33, 150, 83))
311
+ val stroke = cssColor(props.opt("strokeColor"), Color.rgb(33, 150, 83))
312
+ properties.put("kind", "polygon").put("fillColor", fill).put("strokeColor", stroke).put("strokeWidth", width)
313
+ val ring = points + points.first()
314
+ geometry.put("type", "Polygon").put("coordinates", JSONArray().put(JSONArray().apply { ring.forEach { put(JSONArray().put(it.longitude).put(it.latitude)) } }))
315
+ pointsForFit.addAll(points)
239
316
  } else continue
240
317
  features.put(JSONObject().put("type", "Feature").put("properties", properties).put("geometry", geometry))
241
318
  }
@@ -244,10 +321,20 @@ internal class CraftNativeMapView(context: Context) : FrameLayout(context), OnMa
244
321
  }
245
322
 
246
323
  private fun cssColor(value: Any?, fallback: Int): String {
247
- val color = (value as? String)?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback
324
+ val color = (value as? String)?.let { mapColor(it, fallback) } ?: fallback
248
325
  return "rgba(${Color.red(color)},${Color.green(color)},${Color.blue(color)},${Color.alpha(color) / 255.0})"
249
326
  }
250
327
 
328
+ private fun mapColor(raw: String, fallback: Int): Int {
329
+ val value = raw.trim().lowercase()
330
+ val dark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
331
+ return when (value) {
332
+ "label" -> if (dark) Color.WHITE else Color.BLACK
333
+ "secondarylabel", "secondary-label" -> if (dark) Color.LTGRAY else Color.DKGRAY
334
+ else -> runCatching { craftNativeColor(raw) }.getOrDefault(fallback)
335
+ }
336
+ }
337
+
251
338
  private fun directionBitmap(): Bitmap {
252
339
  val size = (26 * resources.displayMetrics.density).roundToInt().coerceAtLeast(24)
253
340
  val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
@@ -111,6 +111,14 @@ class CraftNativeMutationDocument {
111
111
  moveChild(operation, candidateNodes)
112
112
  operation.optString("parentId").takeIf { it !in affectedNodeIds }?.let { affectedNodeIds.add(it) }
113
113
  }
114
+ "reparentChild" -> {
115
+ val previousParent = reparentChild(operation, candidateNodes)
116
+ operation.optString("parentId").takeIf { it !in affectedNodeIds }?.let { affectedNodeIds.add(it) }
117
+ previousParent.takeIf { it !in affectedNodeIds }?.let { affectedNodeIds.add(it) }
118
+ // A child crossing parents needs the committed tree to
119
+ // be reconciled so the host can move its existing view.
120
+ requiresFullRender = true
121
+ }
114
122
  "removeNode" -> {
115
123
  val parent = candidateNodes[operation.optString("id")]?.parent
116
124
  if (parent == null) requiresFullRender = true
@@ -152,6 +160,9 @@ class CraftNativeMutationDocument {
152
160
 
153
161
  fun node(id: String): JSONObject? = materialize(id, nodes)
154
162
 
163
+ /** Reserve surviving protocol nodes before reusing an authored keyed slot. */
164
+ fun containsNode(id: String): Boolean = nodes.containsKey(id)
165
+
155
166
  /** The committed parent used to redraw a child that has no native view. */
156
167
  fun parentId(id: String): String? = nodes[id]?.parent
157
168
 
@@ -225,6 +236,27 @@ class CraftNativeMutationDocument {
225
236
  parent.children.add(insertionIndex(operation, parent.children.size), childId)
226
237
  }
227
238
 
239
+ private fun reparentChild(operation: JSONObject, nodes: MutableMap<String, Node>): String {
240
+ val parentId = requiredId(operation, "parentId", "reparentChild")
241
+ val childId = requiredId(operation, "childId", "reparentChild")
242
+ val parent = nodes[parentId] ?: fail("UNKNOWN_PARENT", "node $parentId does not exist")
243
+ val child = nodes[childId] ?: fail("UNKNOWN_NODE", "node $childId does not exist")
244
+ val previousParentId = child.parent
245
+ ?: fail("NOT_ATTACHED", "node $childId is not attached to a parent")
246
+ val previousParent = nodes[previousParentId]
247
+ ?: fail("INVALID_TREE", "node $childId has a missing parent")
248
+ if (!acceptsChildren(parent.type)) fail("INVALID_PARENT", "node $parentId cannot contain child nodes")
249
+ if (parentId == childId || parentId in descendants(childId, nodes)) {
250
+ fail("CYCLE", "reparenting node $childId would create a cycle")
251
+ }
252
+ if (parentId == previousParentId) fail("SAME_PARENT", "node $childId is already a child of $parentId")
253
+ val index = insertionIndex(operation, parent.children.size)
254
+ previousParent.children.remove(childId)
255
+ parent.children.add(index, childId)
256
+ child.parent = parentId
257
+ return previousParentId
258
+ }
259
+
228
260
  private fun removeNode(
229
261
  operation: JSONObject,
230
262
  nodes: MutableMap<String, Node>,
@@ -22,6 +22,12 @@ internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLa
22
22
  if (handler == null) isRefreshing = false
23
23
  }
24
24
 
25
+ fun resetForTeardown() {
26
+ refreshHandler = null
27
+ isRefreshing = false
28
+ isEnabled = false
29
+ }
30
+
25
31
  fun setRefreshAppearance(props: JSONObject, fallback: Int) {
26
32
  val tint = props.optString("tintColor").takeIf { it.isNotBlank() }
27
33
  val resolved = tint?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback
@@ -29,8 +35,7 @@ internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLa
29
35
  }
30
36
 
31
37
  override fun onDetachedFromWindow() {
32
- refreshHandler = null
33
- isRefreshing = false
38
+ resetForTeardown()
34
39
  super.onDetachedFromWindow()
35
40
  }
36
41
  }
@@ -113,6 +113,7 @@ internal class CraftNativeServices(
113
113
  // `health.grantedTypes()` and `health.openSettings()`.
114
114
  add("healthAvailability")
115
115
  }
116
+ if (config.optBoolean("enableWallet")) add("wallet")
116
117
  if (config.optBoolean("enableKeepAwake")) add("keepAwake")
117
118
  if (config.optBoolean("enableGeolocation") || config.optBoolean("enableBackgroundLocation")) {
118
119
  add("geolocation")
@@ -130,6 +131,7 @@ internal class CraftNativeServices(
130
131
  fun handles(module: String): Boolean = module in setOf(
131
132
  "Dialog", "Browser", "Linking", "Share", "Push", "Health", "KeepAwake", "Brightness", "StatusBar", "Speech", "Background",
132
133
  "Location", "Media", "CrashReporter", "HeartRate",
134
+ "Wallet",
133
135
  )
134
136
 
135
137
  fun perform(module: String, method: String, args: JSONArray, completion: (CraftNativeCapabilityResult) -> Unit) {
@@ -144,6 +146,7 @@ internal class CraftNativeServices(
144
146
  "Share" -> share(args.optJSONObject(0) ?: JSONObject(), done)
145
147
  "Push" -> push(method, done)
146
148
  "Health" -> health?.perform(method, args, done) ?: done(failure("CAPABILITY_DISABLED", "Health Connect is not enabled for this app"))
149
+ "Wallet" -> wallet(method, args, done)
147
150
  "KeepAwake" -> keepAwake(args.optBoolean(0, true), done)
148
151
  "Brightness" -> brightness(method, args, done)
149
152
  "StatusBar" -> statusBar(args.optString(0, "default"), done)
@@ -176,6 +179,28 @@ internal class CraftNativeServices(
176
179
  }
177
180
  }
178
181
 
182
+ private fun wallet(method: String, args: JSONArray, done: (CraftNativeCapabilityResult) -> Unit) {
183
+ if (!config.optBoolean("enableWallet")) return done(failure("CAPABILITY_DISABLED", "Wallet is disabled"))
184
+ when (method) {
185
+ "isAvailable" -> done(success(runCatching {
186
+ context.packageManager.getLaunchIntentForPackage("com.google.android.apps.walletnfcrel") != null
187
+ }.getOrDefault(false)))
188
+ "add" -> {
189
+ val source = args.optJSONObject(0) ?: return done(failure("INVALID_ARGUMENT", "Wallet.add needs a source object"))
190
+ val address = source.optString("url").trim()
191
+ if (!address.startsWith("https://")) return done(failure("INVALID_ARGUMENT", "Android Wallet.add needs an HTTPS save URL"))
192
+ val host = activity() ?: return done(failure("NEEDS_ACTIVITY", "No screen is showing to open Wallet"))
193
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(address))
194
+ if (intent.resolveActivity(host.packageManager) == null) return done(failure("UNAVAILABLE", "Google Wallet cannot open this save URL"))
195
+ runCatching { host.startActivity(intent) }
196
+ .onSuccess { done(success(true)) }
197
+ .onFailure { done(failure("PRESENTATION_FAILED", it.message ?: "Google Wallet could not open")) }
198
+ }
199
+ "has" -> done(failure("UNSUPPORTED", "Google Wallet pass lookup is not available through the save-link API"))
200
+ else -> done(failure("UNKNOWN_ACTION", "Unsupported Wallet method"))
201
+ }
202
+ }
203
+
179
204
  // MARK: Dialogs
180
205
 
181
206
  private fun dialog(method: String, options: JSONObject, done: (CraftNativeCapabilityResult) -> Unit) {
@@ -447,6 +447,24 @@ internal object CraftNativePrelude {
447
447
  // address the host through `window.craft`. Keep native screens
448
448
  // compatible without pretending a document exists.
449
449
  if (typeof globalThis.window === 'undefined') globalThis.window = globalThis;
450
+ // JavaScriptSandbox intentionally has no browser URL/encoding
451
+ // globals. Install named failures instead of letting a shared module
452
+ // die with an unhelpful `URLSearchParams is not defined` halfway
453
+ // through its load path. The full browser implementations belong to
454
+ // the shared STX runtime; these diagnostics keep the native boundary
455
+ // explicit until that runtime is present.
456
+ (function() {
457
+ function unavailable(name, hint) {
458
+ var failure = function() {
459
+ throw new ReferenceError(name + ' is not available on a native screen — ' + hint);
460
+ };
461
+ failure.prototype.constructor = failure;
462
+ return failure;
463
+ }
464
+ if (typeof globalThis.URL === 'undefined') globalThis.URL = unavailable('URL', 'use an absolute URL or build the path with encodeURIComponent');
465
+ if (typeof globalThis.URLSearchParams === 'undefined') globalThis.URLSearchParams = unavailable('URLSearchParams', 'build query strings with encodeURIComponent');
466
+ if (typeof globalThis.TextEncoder === 'undefined') globalThis.TextEncoder = unavailable('TextEncoder', 'use a native-supported encoding helper');
467
+ })();
450
468
  (function() {
451
469
  var forward = null;
452
470
  var listeners = [];
@@ -671,9 +689,11 @@ internal object CraftNativePrelude {
671
689
  // Older native bundles call the page-shaped `craft.dialog` API
672
690
  // directly. Keep that API on the capability bridge so JavaScript
673
691
  // Core/JavaScriptSandbox screens never fall back to a DOM sheet.
674
- if (Array.isArray(__stxNativeBridge.capabilities) && __stxNativeBridge.capabilities.indexOf('dialog') >= 0 && !craft.dialog) {
675
- var nextDialogRequest = 0;
676
- function dialogRequest(method, args) {
692
+ // Native capability ownership wins over any browser-shaped dialog
693
+ // object the bundle installed before the bridge ran. Otherwise an
694
+ // action sheet silently falls back to the DOM renderer.
695
+ var nextDialogRequest = 0;
696
+ function dialogRequest(method, args) {
677
697
  return new Promise(function(resolve, reject) {
678
698
  var id = 'craft-dialog-' + (++nextDialogRequest);
679
699
  var remove = __stxNativeBridge.addMessageListener(function(raw) {
@@ -702,13 +722,19 @@ internal object CraftNativePrelude {
702
722
  },
703
723
  }));
704
724
  });
725
+ }
726
+ function installNativeDialog() {
727
+ var craft = globalThis.craft = globalThis.craft || {};
728
+ if (Array.isArray(__stxNativeBridge.capabilities) && __stxNativeBridge.capabilities.indexOf('dialog') >= 0) {
729
+ craft.dialog = {
730
+ alert: function(options) { return dialogRequest('alert', [options || {}]).then(function() {}); },
731
+ confirm: function(options) { return dialogRequest('confirm', [options || {}]).then(Boolean); },
732
+ actionSheet: function(options) { return dialogRequest('actionSheet', [options || {}]).then(function(id) { return typeof id === 'string' ? id : null; }); },
733
+ };
705
734
  }
706
- craft.dialog = {
707
- alert: function(options) { return dialogRequest('alert', [options || {}]).then(function() {}); },
708
- confirm: function(options) { return dialogRequest('confirm', [options || {}]).then(Boolean); },
709
- actionSheet: function(options) { return dialogRequest('actionSheet', [options || {}]).then(function(id) { return typeof id === 'string' ? id : null; }); },
710
- };
711
735
  }
736
+ globalThis.__craftInstallNativeDialog = installNativeDialog;
737
+ installNativeDialog();
712
738
  })();
713
739
  """.trimIndent()
714
740
  }