craft-native 0.0.301 → 0.0.319
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 +2 -2
- package/dist/android/templates/CraftNativeShell.kt.template +26 -8
- package/dist/android/templates/MainActivityNative.kt.template +119 -6
- package/dist/cli.js +27 -2
- package/dist/index.cjs +28 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +28 -2
- package/dist/ios/src/index.js +6 -1
- package/dist/ios/src/testing.d.ts +6 -0
- package/dist/ios/templates/CraftApp.swift +44 -6
- package/dist/ios/templates/CraftHybrid.swift +6 -0
- package/dist/ios/templates/CraftNativeActions.swift +1 -0
- package/dist/ios/templates/CraftNativeScreen.swift +119 -8
- package/dist/ios/templates/testing/DriverTests.swift.template +38 -0
- package/dist/package.d.ts +15 -4
- package/dist/types.d.ts +28 -9
- package/package.json +1 -1
|
@@ -286,12 +286,12 @@ async function createAndroidEmulator(options) {
|
|
|
286
286
|
if (options.acceptLicenses)
|
|
287
287
|
await run([options.sdkmanager, `--sdk_root=${sdk}`, "--licenses"], `y
|
|
288
288
|
`.repeat(100));
|
|
289
|
-
await run([options.sdkmanager, `--sdk_root=${sdk}`, "platform-tools", "emulator"], options.acceptLicenses ? `y
|
|
289
|
+
await run([options.sdkmanager, `--sdk_root=${sdk}`, "--verbose", "platform-tools", "emulator"], options.acceptLicenses ? `y
|
|
290
290
|
`.repeat(100) : undefined);
|
|
291
291
|
const acceleration = await run([executable, "-accel-check"], undefined, 30000, true);
|
|
292
292
|
if (options.acceleration === "hardware" && acceleration.code !== 0)
|
|
293
293
|
throw new Error(`Android hardware acceleration is unavailable; inspect ${artifacts}`);
|
|
294
|
-
await run([options.sdkmanager, `--sdk_root=${sdk}`, options.image], options.acceptLicenses ? `y
|
|
294
|
+
await run([options.sdkmanager, `--sdk_root=${sdk}`, "--verbose", options.image], options.acceptLicenses ? `y
|
|
295
295
|
`.repeat(100) : undefined);
|
|
296
296
|
const devices = (await run([adb, "devices"], undefined, 1e4)).stdout;
|
|
297
297
|
if (devices.split(`
|
|
@@ -806,6 +806,10 @@ internal class CraftNativeChrome(
|
|
|
806
806
|
private val density = resources.displayMetrics.density
|
|
807
807
|
private var screenBackgroundColor: Int? = null
|
|
808
808
|
private var selectedTab = 0
|
|
809
|
+
private var tabBarHiddenByScreen = false
|
|
810
|
+
private var visiblePath: String? = null
|
|
811
|
+
private val accessorySelected: Boolean
|
|
812
|
+
get() = accessorySpec?.optString("path")?.let { path -> visiblePath == path || visiblePath?.startsWith(path + "/") == true } == true
|
|
809
813
|
private var swipeTracking = false
|
|
810
814
|
private var swipeIntercepted = false
|
|
811
815
|
private var horizontalScrollDeferred = false
|
|
@@ -969,7 +973,8 @@ internal class CraftNativeChrome(
|
|
|
969
973
|
}
|
|
970
974
|
|
|
971
975
|
/** Draws `options` (a screen's `setOptions`) for a screen `depth` deep in its stack. */
|
|
972
|
-
fun apply(options: JSONObject, depth: Int, fallbackTitle: String?) {
|
|
976
|
+
fun apply(options: JSONObject, depth: Int, fallbackTitle: String?, path: String? = null) {
|
|
977
|
+
visiblePath = path
|
|
973
978
|
screenBackgroundColor = options.opt("backgroundColor")
|
|
974
979
|
?.takeUnless { it == JSONObject.NULL }
|
|
975
980
|
?.let { craftNativeOptionalString(it) }
|
|
@@ -1004,7 +1009,19 @@ internal class CraftNativeChrome(
|
|
|
1004
1009
|
renderedButtonSpecs = buttonSpecs
|
|
1005
1010
|
}
|
|
1006
1011
|
val tabBarHidden = options.optBoolean("tabBarHidden", false) || depth > 0 && options.optBoolean("tabBarHiddenWhenPushed", false)
|
|
1007
|
-
|
|
1012
|
+
if (tabBarHiddenByScreen != tabBarHidden) {
|
|
1013
|
+
tabBarHiddenByScreen = tabBarHidden
|
|
1014
|
+
tabRow.animate().cancel()
|
|
1015
|
+
val duration = if (android.animation.ValueAnimator.areAnimatorsEnabled()) 240L else 0L
|
|
1016
|
+
if (tabBarHidden) {
|
|
1017
|
+
tabRow.animate().translationY(dp(80).toFloat()).alpha(0f).setDuration(duration).withEndAction {
|
|
1018
|
+
if (tabBarHiddenByScreen) tabRow.visibility = View.GONE
|
|
1019
|
+
}.start()
|
|
1020
|
+
} else if (tabs.isNotEmpty()) {
|
|
1021
|
+
tabRow.visibility = View.VISIBLE
|
|
1022
|
+
tabRow.animate().translationY(0f).alpha(1f).setDuration(duration).start()
|
|
1023
|
+
}
|
|
1024
|
+
} else if (tabs.isEmpty()) tabRow.visibility = View.GONE
|
|
1008
1025
|
accessorySpec = options.optJSONObject("tabAccessory")?.takeIf {
|
|
1009
1026
|
val path = it.optString("path")
|
|
1010
1027
|
path.startsWith("/") && !path.startsWith("//") && it.has("title")
|
|
@@ -1091,7 +1108,7 @@ internal class CraftNativeChrome(
|
|
|
1091
1108
|
/** Selection belongs to the native tab stack, including retained pushed screens. */
|
|
1092
1109
|
internal fun tabIconAppearance(index: Int, motionAllowed: Boolean): CraftTabIconAppearance {
|
|
1093
1110
|
val tab = tabs[index]
|
|
1094
|
-
val selected = index == selectedTab
|
|
1111
|
+
val selected = index == selectedTab && !accessorySelected
|
|
1095
1112
|
return CraftTabIconAppearance(
|
|
1096
1113
|
if (selected) selectedSymbols[tab.id] ?: tabSymbols[tab.id] ?: tab.symbol else tabSymbols[tab.id] ?: tab.symbol,
|
|
1097
1114
|
if (selected) selectedIconTints[tab.id] ?: tabIconTints[tab.id] else tabIconTints[tab.id],
|
|
@@ -1114,12 +1131,13 @@ internal class CraftNativeChrome(
|
|
|
1114
1131
|
backButton.imageTintList = ColorStateList.valueOf(accent)
|
|
1115
1132
|
val surface = if (dark) Color.parseColor("#1C1C1E") else Color.parseColor("#F9F9F9")
|
|
1116
1133
|
tabBar.background = GradientDrawable().apply { setColor(surface); cornerRadius = dp(32).toFloat() }
|
|
1117
|
-
accessory.
|
|
1118
|
-
|
|
1134
|
+
accessory.isSelected = accessorySelected
|
|
1135
|
+
accessory.background = GradientDrawable().apply { setColor(if (accessorySelected) Color.argb(36, Color.red(accent), Color.green(accent), Color.blue(accent)) else surface); cornerRadius = dp(32).toFloat() }
|
|
1136
|
+
accessorySpec?.let { accessoryIcon.setImageDrawable(CraftNativeIcons.drawable(context, null, it.optString("symbol"), if (accessorySelected) accent else text)) }
|
|
1119
1137
|
tabViews.forEachIndexed { index, (icon, label) ->
|
|
1120
|
-
tabBar.getChildAt(index).isSelected = index == selectedTab
|
|
1138
|
+
tabBar.getChildAt(index).isSelected = index == selectedTab && !accessorySelected
|
|
1121
1139
|
tabBar.getChildAt(index).background = tabCellBackground(index, dark)
|
|
1122
|
-
val color = if (index == selectedTab) accent else muted
|
|
1140
|
+
val color = if (index == selectedTab && !accessorySelected) accent else muted
|
|
1123
1141
|
val id = tabs[index].id
|
|
1124
1142
|
val number = tabIconText[id]
|
|
1125
1143
|
val appearance = tabIconAppearance(index, hasWindowFocus() && tabRow.isShown && android.animation.ValueAnimator.areAnimatorsEnabled())
|
|
@@ -1209,7 +1227,7 @@ internal class CraftNativeChrome(
|
|
|
1209
1227
|
private fun accentColor(dark: Boolean): Int = if (dark) Color.parseColor("#0A84FF") else Color.parseColor("#007AFF")
|
|
1210
1228
|
|
|
1211
1229
|
private fun tabCellBackground(index: Int, dark: Boolean): android.graphics.drawable.Drawable? {
|
|
1212
|
-
if (index != selectedTab || (tabBarAppearance.selectionColor == null && tabBarAppearance.selectionCornerRadius == null)) return rippleBackground()
|
|
1230
|
+
if (accessorySelected || index != selectedTab || (tabBarAppearance.selectionColor == null && tabBarAppearance.selectionCornerRadius == null)) return rippleBackground()
|
|
1213
1231
|
val color = tabBarAppearance.selectionColor?.let { runCatching { Color.parseColor(it) }.getOrNull() }
|
|
1214
1232
|
?: if (dark) Color.argb(42, 255, 255, 255) else Color.argb(23, 0, 0, 0)
|
|
1215
1233
|
val radius = tabBarAppearance.selectionCornerRadius?.let { (it * density).toInt().toFloat() } ?: dp(32).toFloat()
|
|
@@ -864,6 +864,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
864
864
|
super.onWindowVisibilityChanged(visibility)
|
|
865
865
|
if (visibility != View.VISIBLE) celebration?.stop()
|
|
866
866
|
}
|
|
867
|
+
var pageViewport: Pair<Int, Int>? = null
|
|
867
868
|
var onLayoutChanged: (() -> Unit)? = null
|
|
868
869
|
private val styles = mutableMapOf<View, CraftNativeLayoutStyle>()
|
|
869
870
|
private var wrap = false
|
|
@@ -1839,6 +1840,12 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1839
1840
|
}
|
|
1840
1841
|
|
|
1841
1842
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
1843
|
+
pageViewport?.let { (pageWidth, pageHeight) ->
|
|
1844
|
+
val pages = (0 until childCount).map { getChildAt(it) }.filter { it.visibility != View.GONE }
|
|
1845
|
+
pages.forEach { it.measure(MeasureSpec.makeMeasureSpec(pageWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(pageHeight, MeasureSpec.EXACTLY)) }
|
|
1846
|
+
setMeasuredDimension(pageWidth * pages.size, pageHeight)
|
|
1847
|
+
return
|
|
1848
|
+
}
|
|
1842
1849
|
val unboundedHorizontal = unboundedWidth && orientation == HORIZONTAL
|
|
1843
1850
|
val widthMode = if (unboundedHorizontal) View.MeasureSpec.UNSPECIFIED else View.MeasureSpec.getMode(widthMeasureSpec)
|
|
1844
1851
|
widthDefinite = widthMode == View.MeasureSpec.EXACTLY
|
|
@@ -1875,6 +1882,12 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1875
1882
|
|
|
1876
1883
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
1877
1884
|
celebration?.layout()
|
|
1885
|
+
pageViewport?.let { (pageWidth, pageHeight) ->
|
|
1886
|
+
val pages = (0 until childCount).map { getChildAt(it) }.filter { it.visibility != View.GONE }
|
|
1887
|
+
pages.forEachIndexed { index, child -> child.layout(index * pageWidth, 0, (index + 1) * pageWidth, pageHeight) }
|
|
1888
|
+
onLayoutChanged?.invoke()
|
|
1889
|
+
return
|
|
1890
|
+
}
|
|
1878
1891
|
widthDefinite = true
|
|
1879
1892
|
if (grid) {
|
|
1880
1893
|
val layout = gridLayout(width, height)
|
|
@@ -2237,15 +2250,92 @@ private class CraftNativeKeyboardAvoidingView(context: Context) : FrameLayout(co
|
|
|
2237
2250
|
}
|
|
2238
2251
|
}
|
|
2239
2252
|
|
|
2240
|
-
|
|
2253
|
+
/** CSS family fallback order, using bundled app fonts before platform families. */
|
|
2254
|
+
internal fun craftNativeFontFamily(family: String?, bundledFonts: Map<String, Typeface> = emptyMap()): Typeface =
|
|
2255
|
+
family?.split(',')?.asSequence()?.map { it.trim().trim('"', '\'') }?.mapNotNull { name ->
|
|
2256
|
+
bundledFonts[name] ?: when (name.lowercase()) {
|
|
2257
|
+
"monospace", "ui-monospace" -> Typeface.MONOSPACE
|
|
2258
|
+
"serif", "ui-serif" -> Typeface.SERIF
|
|
2259
|
+
"sans-serif", "ui-sans-serif", "system-ui", "-apple-system" -> Typeface.SANS_SERIF
|
|
2260
|
+
else -> Typeface.create(name, Typeface.NORMAL).takeUnless { it == Typeface.DEFAULT }
|
|
2261
|
+
}
|
|
2262
|
+
}?.firstOrNull() ?: Typeface.DEFAULT
|
|
2263
|
+
|
|
2264
|
+
internal class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(context) {
|
|
2241
2265
|
val content = CraftNativeFlexLayout(context)
|
|
2242
2266
|
// Keep one refresh target across axis changes. Replacing all children of
|
|
2243
2267
|
// SwipeRefreshLayout would also detach its own animated indicator.
|
|
2244
2268
|
private val scrollContainer = FrameLayout(context)
|
|
2245
|
-
private val vertical = ScrollView(context)
|
|
2246
|
-
|
|
2269
|
+
private val vertical = object : ScrollView(context) {
|
|
2270
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean = isEnabled && super.onInterceptTouchEvent(event)
|
|
2271
|
+
override fun onTouchEvent(event: MotionEvent): Boolean = isEnabled && super.onTouchEvent(event)
|
|
2272
|
+
}
|
|
2273
|
+
private val horizontal = object : HorizontalScrollView(context) {
|
|
2274
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean = isEnabled && super.onInterceptTouchEvent(event)
|
|
2275
|
+
override fun onTouchEvent(event: MotionEvent): Boolean = isEnabled && super.onTouchEvent(event)
|
|
2276
|
+
}
|
|
2247
2277
|
private var horizontalMode: Boolean? = null
|
|
2248
2278
|
private var pagingEnabled = false
|
|
2279
|
+
private var authoredPageIndex: Int? = null
|
|
2280
|
+
private var pendingPageIndex: Int? = null
|
|
2281
|
+
private var currentPageIndex = 0
|
|
2282
|
+
private var reportedPage: Pair<Int, Int>? = null
|
|
2283
|
+
private var pageHandler: ((Int, Int) -> Unit)? = null
|
|
2284
|
+
private var pageSettleGeneration = 0
|
|
2285
|
+
private var pageDragging = false
|
|
2286
|
+
private var pageInMotion = false
|
|
2287
|
+
|
|
2288
|
+
private fun schedulePageSettle(scroller: View) {
|
|
2289
|
+
val generation = ++pageSettleGeneration
|
|
2290
|
+
scroller.postDelayed({
|
|
2291
|
+
if (generation == pageSettleGeneration && !pageDragging) {
|
|
2292
|
+
pageInMotion = false
|
|
2293
|
+
settlePage()
|
|
2294
|
+
}
|
|
2295
|
+
}, 80L)
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
fun configurePager(value: Any?, onSelect: ((Int, Int) -> Unit)?) {
|
|
2299
|
+
val next = (value as? Number)?.toDouble()?.takeIf { it.isFinite() }?.coerceIn(0.0, (Int.MAX_VALUE / 2).toDouble())?.toInt()
|
|
2300
|
+
if (next != authoredPageIndex) pendingPageIndex = next
|
|
2301
|
+
authoredPageIndex = next
|
|
2302
|
+
pageHandler = onSelect
|
|
2303
|
+
if (next == null) { content.pageViewport = null; reportedPage = null }
|
|
2304
|
+
requestLayout()
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
private fun settlePage() {
|
|
2308
|
+
if (authoredPageIndex == null || horizontal.width <= 0 || pageDragging || pageInMotion) return
|
|
2309
|
+
val pages = (0 until content.childCount).map { content.getChildAt(it) }.filter { it.visibility != View.GONE }
|
|
2310
|
+
val index = (horizontal.scrollX.toFloat() / horizontal.width).roundToInt().coerceIn(0, max(0, pages.size - 1))
|
|
2311
|
+
val target = index * horizontal.width
|
|
2312
|
+
if (horizontal.scrollX != target) {
|
|
2313
|
+
// ACTION_UP can start native motion after our touch listener's snap.
|
|
2314
|
+
// A rounded index is not settled until that viewport is fully shown.
|
|
2315
|
+
pageInMotion = true
|
|
2316
|
+
horizontal.smoothScrollTo(target, 0)
|
|
2317
|
+
schedulePageSettle(horizontal)
|
|
2318
|
+
return
|
|
2319
|
+
}
|
|
2320
|
+
currentPageIndex = index
|
|
2321
|
+
pages.forEachIndexed { i, page -> page.importantForAccessibility = if (i == index) View.IMPORTANT_FOR_ACCESSIBILITY_AUTO else View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS }
|
|
2322
|
+
val report = index to pages.size
|
|
2323
|
+
if (report != reportedPage) { reportedPage = report; pageHandler?.invoke(index, pages.size) }
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
2327
|
+
val oldWidth = horizontal.width
|
|
2328
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
2329
|
+
if (authoredPageIndex != null && horizontalMode == true && horizontal.width > 0) {
|
|
2330
|
+
val count = (0 until content.childCount).count { content.getChildAt(it).visibility != View.GONE }
|
|
2331
|
+
val index = (pendingPageIndex ?: currentPageIndex).coerceIn(0, max(0, count - 1))
|
|
2332
|
+
if (pendingPageIndex != null || oldWidth != horizontal.width || index != currentPageIndex) horizontal.scrollTo(index * horizontal.width, 0)
|
|
2333
|
+
pendingPageIndex = null
|
|
2334
|
+
currentPageIndex = index
|
|
2335
|
+
settlePage()
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2249
2339
|
private var bounces = true
|
|
2250
2340
|
private var alwaysBounceVertical = true
|
|
2251
2341
|
private var alwaysBounceHorizontal = true
|
|
@@ -2273,6 +2363,10 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2273
2363
|
// configured. Interception is gated below instead.
|
|
2274
2364
|
isEnabled = true
|
|
2275
2365
|
setHorizontal(false)
|
|
2366
|
+
// Paging is owned by this native control even before an Activity
|
|
2367
|
+
// supplies optional bridge callbacks. Its normal configuration may
|
|
2368
|
+
// replace these listeners without changing gesture ownership.
|
|
2369
|
+
setScrollListeners(null, null, null, null, null, "none", null)
|
|
2276
2370
|
}
|
|
2277
2371
|
|
|
2278
2372
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
@@ -2301,6 +2395,7 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2301
2395
|
val unspecified = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
2302
2396
|
val horizontalPadding = paddingLeft + paddingRight
|
|
2303
2397
|
val verticalPadding = paddingTop + paddingBottom
|
|
2398
|
+
content.pageViewport = if (authoredPageIndex != null && sideways) max(0, widthSize - horizontalPadding) to max(0, heightSize - verticalPadding) else null
|
|
2304
2399
|
content.measure(
|
|
2305
2400
|
if (sideways || widthMode == MeasureSpec.UNSPECIFIED) unspecified
|
|
2306
2401
|
else MeasureSpec.makeMeasureSpec(max(0, widthSize - horizontalPadding), MeasureSpec.EXACTLY),
|
|
@@ -2316,7 +2411,7 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2316
2411
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
2317
2412
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
2318
2413
|
)
|
|
2319
|
-
params.width = max(params.width, content.measuredWidth)
|
|
2414
|
+
params.width = if (authoredPageIndex != null) content.measuredWidth else max(params.width, content.measuredWidth)
|
|
2320
2415
|
content.layoutParams = params
|
|
2321
2416
|
}
|
|
2322
2417
|
fun fit(mode: Int, size: Int, wanted: Int): Int = when (mode) {
|
|
@@ -2648,11 +2743,18 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2648
2743
|
|
|
2649
2744
|
scroller.setOnScrollChangeListener { _, x, y, _, _ ->
|
|
2650
2745
|
if (horizontalMode != true) updateStickyHeaders()
|
|
2746
|
+
if (authoredPageIndex != null) {
|
|
2747
|
+
pageInMotion = true
|
|
2748
|
+
schedulePageSettle(scroller)
|
|
2749
|
+
}
|
|
2651
2750
|
onScroll?.invoke(x, y)
|
|
2652
2751
|
}
|
|
2653
2752
|
scroller.setOnTouchListener { _, event ->
|
|
2654
2753
|
when (event.actionMasked) {
|
|
2655
2754
|
android.view.MotionEvent.ACTION_DOWN -> {
|
|
2755
|
+
pageDragging = true
|
|
2756
|
+
pageInMotion = false
|
|
2757
|
+
pageSettleGeneration += 1
|
|
2656
2758
|
if (momentumActive) {
|
|
2657
2759
|
momentumActive = false
|
|
2658
2760
|
onMomentumEnd?.invoke()
|
|
@@ -2674,6 +2776,8 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2674
2776
|
}
|
|
2675
2777
|
onEndDrag?.invoke()
|
|
2676
2778
|
if (pagingEnabled) snapToPage(scroller)
|
|
2779
|
+
pageDragging = false
|
|
2780
|
+
if (authoredPageIndex != null) schedulePageSettle(scroller)
|
|
2677
2781
|
if (moved && !momentumActive) {
|
|
2678
2782
|
momentumActive = true
|
|
2679
2783
|
lastX = scroller.scrollX
|
|
@@ -2692,6 +2796,10 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2692
2796
|
|
|
2693
2797
|
fun clearScrollListeners() {
|
|
2694
2798
|
scrollListenerGeneration += 1
|
|
2799
|
+
pageSettleGeneration += 1
|
|
2800
|
+
pageDragging = false
|
|
2801
|
+
pageInMotion = false
|
|
2802
|
+
pageHandler = null
|
|
2695
2803
|
vertical.setOnScrollChangeListener(null)
|
|
2696
2804
|
vertical.setOnTouchListener(null)
|
|
2697
2805
|
horizontal.setOnScrollChangeListener(null)
|
|
@@ -4868,7 +4976,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4868
4976
|
val bars = chrome ?: return
|
|
4869
4977
|
val depth = screens.indexOf(screen).coerceAtLeast(0)
|
|
4870
4978
|
val fallback = if (depth == 0) routes.tabs.getOrNull(currentTab)?.title else null
|
|
4871
|
-
bars.apply(screen.options, depth, fallback)
|
|
4979
|
+
bars.apply(screen.options, depth, fallback, screen.path)
|
|
4872
4980
|
}
|
|
4873
4981
|
|
|
4874
4982
|
/** Applies a screen-specific background, falling back to the dynamic app colour. */
|
|
@@ -4932,6 +5040,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4932
5040
|
put("viewReparent")
|
|
4933
5041
|
put("viewNonModalPresentation")
|
|
4934
5042
|
put("nativePlatformControls")
|
|
5043
|
+
put("nativePager")
|
|
4935
5044
|
}
|
|
4936
5045
|
|
|
4937
5046
|
private fun appVersion(): Pair<String, String> = runCatching {
|
|
@@ -6095,6 +6204,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
6095
6204
|
craftNativeBoolean(props.opt("alwaysBounceHorizontal"), horizontal),
|
|
6096
6205
|
)
|
|
6097
6206
|
setPagingEnabled(craftNativeBoolean(props.opt("pagingEnabled")))
|
|
6207
|
+
val pageHandler = craftNativeEventName(events, "onPageSelected").takeIf { it.isNotBlank() }
|
|
6208
|
+
configurePager(props.opt("pageIndex"), pageHandler?.let { handler ->
|
|
6209
|
+
{ index, count -> send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject().put("index", index).put("count", count))) }
|
|
6210
|
+
})
|
|
6098
6211
|
val refreshNode = (0 until children.length()).asSequence()
|
|
6099
6212
|
.mapNotNull { children.optJSONObject(it) }
|
|
6100
6213
|
.firstOrNull { canonicalType(it.optString("type")) == "RefreshControl" }
|
|
@@ -6916,7 +7029,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6916
7029
|
}
|
|
6917
7030
|
val italic = craftNativeOptionalString(style.opt("fontStyle")) == "italic"
|
|
6918
7031
|
val family = craftNativeOptionalString(style.opt("fontFamily"))?.takeIf { it.isNotBlank() }
|
|
6919
|
-
val base =
|
|
7032
|
+
val base = craftNativeFontFamily(family, bundledFonts)
|
|
6920
7033
|
view.typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
6921
7034
|
Typeface.create(base, weight, italic)
|
|
6922
7035
|
} else Typeface.create(base, (if (weight >= 600) Typeface.BOLD else Typeface.NORMAL) or
|
package/dist/cli.js
CHANGED
|
@@ -262,6 +262,7 @@ async function packageMacOS(config, outDir) {
|
|
|
262
262
|
iconPath: config.iconPath,
|
|
263
263
|
provisioningProfile: opts.provisioningProfile,
|
|
264
264
|
additionalExecutables: opts.additionalExecutables,
|
|
265
|
+
nativeRuntimePath: opts.nativeRuntimePath,
|
|
265
266
|
outputPath: appBundlePath
|
|
266
267
|
});
|
|
267
268
|
if (!appBundle.success) {
|
|
@@ -543,7 +544,7 @@ ${schemes.length ? `MimeType=${schemes.map((scheme) => `x-scheme-handler/${schem
|
|
|
543
544
|
}
|
|
544
545
|
function macOSInfoPlist(metadata) {
|
|
545
546
|
const entries = [
|
|
546
|
-
["CFBundleExecutable", metadata.name],
|
|
547
|
+
["CFBundleExecutable", metadata.executableName || metadata.name],
|
|
547
548
|
["CFBundleIdentifier", metadata.bundleId],
|
|
548
549
|
["CFBundleName", metadata.name],
|
|
549
550
|
["CFBundleShortVersionString", metadata.version],
|
|
@@ -686,6 +687,9 @@ function copyMacOSAppBundle(source, destination) {
|
|
|
686
687
|
return { success: false, error: error.message };
|
|
687
688
|
}
|
|
688
689
|
}
|
|
690
|
+
function macOSRuntimeExecutablePath(appBundlePath) {
|
|
691
|
+
return join2(appBundlePath, "Contents", "Helpers", "CraftRuntime.app", "Contents", "MacOS", "craft");
|
|
692
|
+
}
|
|
689
693
|
function createMacOSAppBundle(opts) {
|
|
690
694
|
try {
|
|
691
695
|
const { name, binaryPath, iconPath, provisioningProfile, additionalExecutables, outputPath } = opts;
|
|
@@ -702,6 +706,27 @@ function createMacOSAppBundle(opts) {
|
|
|
702
706
|
copyFileSync(executable, destination);
|
|
703
707
|
chmodSync(destination, 493);
|
|
704
708
|
}
|
|
709
|
+
if (opts.nativeRuntimePath) {
|
|
710
|
+
const helper = macOSRuntimeExecutablePath(outputPath);
|
|
711
|
+
const helperContents = resolve(helper, "..", "..");
|
|
712
|
+
mkdirSync(join2(helperContents, "MacOS"), { recursive: true });
|
|
713
|
+
copyFileSync(opts.nativeRuntimePath, helper);
|
|
714
|
+
chmodSync(helper, 493);
|
|
715
|
+
if (iconPath) {
|
|
716
|
+
mkdirSync(join2(helperContents, "Resources"), { recursive: true });
|
|
717
|
+
copyFileSync(iconPath, join2(helperContents, "Resources", "icon.icns"));
|
|
718
|
+
}
|
|
719
|
+
writeFileSync3(join2(helperContents, "Info.plist"), macOSInfoPlist({
|
|
720
|
+
name,
|
|
721
|
+
executableName: "craft",
|
|
722
|
+
version: opts.version,
|
|
723
|
+
bundleId: `${opts.bundleId}.runtime`,
|
|
724
|
+
buildNumber: opts.buildNumber,
|
|
725
|
+
minimumSystemVersion: opts.minimumSystemVersion,
|
|
726
|
+
menuBarOnly: true,
|
|
727
|
+
iconName: iconPath ? "icon" : undefined
|
|
728
|
+
}));
|
|
729
|
+
}
|
|
705
730
|
let iconName;
|
|
706
731
|
if (iconPath) {
|
|
707
732
|
if (!existsSync4(iconPath))
|
|
@@ -2945,7 +2970,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
2945
2970
|
`);
|
|
2946
2971
|
}
|
|
2947
2972
|
// package.json
|
|
2948
|
-
var version = "0.0.
|
|
2973
|
+
var version = "0.0.319";
|
|
2949
2974
|
|
|
2950
2975
|
// bin/cli.ts
|
|
2951
2976
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
|
@@ -158,6 +158,7 @@ __export(exports_src, {
|
|
|
158
158
|
liveActivities: () => liveActivities,
|
|
159
159
|
loadURL: () => loadURL,
|
|
160
160
|
location: () => location,
|
|
161
|
+
macOSRuntimeExecutablePath: () => macOSRuntimeExecutablePath,
|
|
161
162
|
materialYou: () => materialYou,
|
|
162
163
|
media: () => media,
|
|
163
164
|
menuItem: () => menuItem,
|
|
@@ -374,7 +375,7 @@ function craftBinaryNotFoundMessage(triedPath) {
|
|
|
374
375
|
`);
|
|
375
376
|
}
|
|
376
377
|
// package.json
|
|
377
|
-
var version = "0.0.
|
|
378
|
+
var version = "0.0.319";
|
|
378
379
|
|
|
379
380
|
// src/sdk-version.ts
|
|
380
381
|
var SDK_VERSION = version;
|
|
@@ -627,6 +628,7 @@ async function packageMacOS(config, outDir) {
|
|
|
627
628
|
iconPath: config.iconPath,
|
|
628
629
|
provisioningProfile: opts.provisioningProfile,
|
|
629
630
|
additionalExecutables: opts.additionalExecutables,
|
|
631
|
+
nativeRuntimePath: opts.nativeRuntimePath,
|
|
630
632
|
outputPath: appBundlePath
|
|
631
633
|
});
|
|
632
634
|
if (!appBundle.success) {
|
|
@@ -909,7 +911,7 @@ ${schemes.length ? `MimeType=${schemes.map((scheme) => `x-scheme-handler/${schem
|
|
|
909
911
|
}
|
|
910
912
|
function macOSInfoPlist(metadata) {
|
|
911
913
|
const entries = [
|
|
912
|
-
["CFBundleExecutable", metadata.name],
|
|
914
|
+
["CFBundleExecutable", metadata.executableName || metadata.name],
|
|
913
915
|
["CFBundleIdentifier", metadata.bundleId],
|
|
914
916
|
["CFBundleName", metadata.name],
|
|
915
917
|
["CFBundleShortVersionString", metadata.version],
|
|
@@ -1052,6 +1054,9 @@ function copyMacOSAppBundle(source, destination) {
|
|
|
1052
1054
|
return { success: false, error: error.message };
|
|
1053
1055
|
}
|
|
1054
1056
|
}
|
|
1057
|
+
function macOSRuntimeExecutablePath(appBundlePath) {
|
|
1058
|
+
return import_path.join(appBundlePath, "Contents", "Helpers", "CraftRuntime.app", "Contents", "MacOS", "craft");
|
|
1059
|
+
}
|
|
1055
1060
|
function createMacOSAppBundle(opts) {
|
|
1056
1061
|
try {
|
|
1057
1062
|
const { name, binaryPath, iconPath, provisioningProfile, additionalExecutables, outputPath } = opts;
|
|
@@ -1068,6 +1073,27 @@ function createMacOSAppBundle(opts) {
|
|
|
1068
1073
|
import_fs.copyFileSync(executable, destination);
|
|
1069
1074
|
import_fs.chmodSync(destination, 493);
|
|
1070
1075
|
}
|
|
1076
|
+
if (opts.nativeRuntimePath) {
|
|
1077
|
+
const helper = macOSRuntimeExecutablePath(outputPath);
|
|
1078
|
+
const helperContents = import_path.resolve(helper, "..", "..");
|
|
1079
|
+
import_fs.mkdirSync(import_path.join(helperContents, "MacOS"), { recursive: true });
|
|
1080
|
+
import_fs.copyFileSync(opts.nativeRuntimePath, helper);
|
|
1081
|
+
import_fs.chmodSync(helper, 493);
|
|
1082
|
+
if (iconPath) {
|
|
1083
|
+
import_fs.mkdirSync(import_path.join(helperContents, "Resources"), { recursive: true });
|
|
1084
|
+
import_fs.copyFileSync(iconPath, import_path.join(helperContents, "Resources", "icon.icns"));
|
|
1085
|
+
}
|
|
1086
|
+
import_fs.writeFileSync(import_path.join(helperContents, "Info.plist"), macOSInfoPlist({
|
|
1087
|
+
name,
|
|
1088
|
+
executableName: "craft",
|
|
1089
|
+
version: opts.version,
|
|
1090
|
+
bundleId: `${opts.bundleId}.runtime`,
|
|
1091
|
+
buildNumber: opts.buildNumber,
|
|
1092
|
+
minimumSystemVersion: opts.minimumSystemVersion,
|
|
1093
|
+
menuBarOnly: true,
|
|
1094
|
+
iconName: iconPath ? "icon" : undefined
|
|
1095
|
+
}));
|
|
1096
|
+
}
|
|
1071
1097
|
let iconName;
|
|
1072
1098
|
if (iconPath) {
|
|
1073
1099
|
if (!import_fs.existsSync(iconPath))
|
package/dist/index.d.cts
CHANGED
|
@@ -24,7 +24,7 @@ export declare function parseCraftVersionOutput(output: string): string;
|
|
|
24
24
|
* (#236).
|
|
25
25
|
*/
|
|
26
26
|
export declare function craftProcessFailedMessage(code: number | null, stderr: string, quiet: boolean): string;
|
|
27
|
-
export { packageApp, pack, type PackageConfig, type PackageResult } from './package.js';
|
|
27
|
+
export { packageApp, pack, macOSRuntimeExecutablePath, type PackageConfig, type PackageResult } from './package.js';
|
|
28
28
|
export { craftBinaryNotFoundMessage, resolveCraftBinary } from './binary-resolver.js';
|
|
29
29
|
export * from './utils/index.js';
|
|
30
30
|
export * from './api/index.js';
|
|
@@ -85,4 +85,4 @@ export declare function show(html: string, options?: WindowOptions): Promise<voi
|
|
|
85
85
|
* Quick helper to load a URL
|
|
86
86
|
*/
|
|
87
87
|
export declare function loadURL(url: string, options?: WindowOptions): Promise<void>;
|
|
88
|
-
export type { WindowOptions, CraftSidebarAPI, CraftNativeUIAPI, CraftContextMenuItem, CraftContextMenuResult, AppConfig, CraftTrayAPI, CraftWindowAPI, CraftAppAPI, CraftBridgeAPI, CraftTimersAPI, Permission, HapticType, CraftHapticsAPI, CameraOptions, PhotoPickerOptions, CraftMobileAPI, CraftFileSystemAPI, CraftFsWatchEvent, CraftFsWatchHandle, CraftDatabaseAPI, CraftHttpAPI, CraftCryptoAPI, CraftCapabilities, CapabilityNamespace, CapabilityNamespaceStatus, CapabilityAction, CraftPreferencesAPI, CraftPreferencesInfo, CraftSettingsAPI, PreferenceValue, CraftEventType, CraftEventMap, CraftEventHandler, CraftEventEmitter, IOSConfig, AndroidConfig, MacOSConfig, WindowsConfig, LinuxConfig, CraftAppConfig, } from './types.js';
|
|
88
|
+
export type { WindowOptions, CraftSidebarAPI, CraftNativeUIAPI, CraftContextMenuItem, CraftContextMenuResult, AppConfig, CraftTrayAPI, CraftWindowAPI, CraftAppAPI, CraftBridgeAPI, MenubarState, MenubarInitOptions, CraftTimersAPI, Permission, HapticType, CraftHapticsAPI, CameraOptions, PhotoPickerOptions, CraftMobileAPI, CraftFileSystemAPI, CraftFsWatchEvent, CraftFsWatchHandle, CraftDatabaseAPI, CraftHttpAPI, CraftCryptoAPI, CraftCapabilities, CapabilityNamespace, CapabilityNamespaceStatus, CapabilityAction, CraftPreferencesAPI, CraftPreferencesInfo, CraftSettingsAPI, PreferenceValue, CraftEventType, CraftEventMap, CraftEventHandler, CraftEventEmitter, IOSConfig, AndroidConfig, MacOSConfig, WindowsConfig, LinuxConfig, CraftAppConfig, } from './types.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare function parseCraftVersionOutput(output: string): string;
|
|
|
24
24
|
* (#236).
|
|
25
25
|
*/
|
|
26
26
|
export declare function craftProcessFailedMessage(code: number | null, stderr: string, quiet: boolean): string;
|
|
27
|
-
export { packageApp, pack, type PackageConfig, type PackageResult } from './package.js';
|
|
27
|
+
export { packageApp, pack, macOSRuntimeExecutablePath, type PackageConfig, type PackageResult } from './package.js';
|
|
28
28
|
export { craftBinaryNotFoundMessage, resolveCraftBinary } from './binary-resolver.js';
|
|
29
29
|
export * from './utils/index.js';
|
|
30
30
|
export * from './api/index.js';
|
|
@@ -85,4 +85,4 @@ export declare function show(html: string, options?: WindowOptions): Promise<voi
|
|
|
85
85
|
* Quick helper to load a URL
|
|
86
86
|
*/
|
|
87
87
|
export declare function loadURL(url: string, options?: WindowOptions): Promise<void>;
|
|
88
|
-
export type { WindowOptions, CraftSidebarAPI, CraftNativeUIAPI, CraftContextMenuItem, CraftContextMenuResult, AppConfig, CraftTrayAPI, CraftWindowAPI, CraftAppAPI, CraftBridgeAPI, CraftTimersAPI, Permission, HapticType, CraftHapticsAPI, CameraOptions, PhotoPickerOptions, CraftMobileAPI, CraftFileSystemAPI, CraftFsWatchEvent, CraftFsWatchHandle, CraftDatabaseAPI, CraftHttpAPI, CraftCryptoAPI, CraftCapabilities, CapabilityNamespace, CapabilityNamespaceStatus, CapabilityAction, CraftPreferencesAPI, CraftPreferencesInfo, CraftSettingsAPI, PreferenceValue, CraftEventType, CraftEventMap, CraftEventHandler, CraftEventEmitter, IOSConfig, AndroidConfig, MacOSConfig, WindowsConfig, LinuxConfig, CraftAppConfig, } from './types.js';
|
|
88
|
+
export type { WindowOptions, CraftSidebarAPI, CraftNativeUIAPI, CraftContextMenuItem, CraftContextMenuResult, AppConfig, CraftTrayAPI, CraftWindowAPI, CraftAppAPI, CraftBridgeAPI, MenubarState, MenubarInitOptions, CraftTimersAPI, Permission, HapticType, CraftHapticsAPI, CameraOptions, PhotoPickerOptions, CraftMobileAPI, CraftFileSystemAPI, CraftFsWatchEvent, CraftFsWatchHandle, CraftDatabaseAPI, CraftHttpAPI, CraftCryptoAPI, CraftCapabilities, CapabilityNamespace, CapabilityNamespaceStatus, CapabilityAction, CraftPreferencesAPI, CraftPreferencesInfo, CraftSettingsAPI, PreferenceValue, CraftEventType, CraftEventMap, CraftEventHandler, CraftEventEmitter, IOSConfig, AndroidConfig, MacOSConfig, WindowsConfig, LinuxConfig, CraftAppConfig, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -117,7 +117,7 @@ function craftBinaryNotFoundMessage(triedPath) {
|
|
|
117
117
|
`);
|
|
118
118
|
}
|
|
119
119
|
// package.json
|
|
120
|
-
var version = "0.0.
|
|
120
|
+
var version = "0.0.319";
|
|
121
121
|
|
|
122
122
|
// src/sdk-version.ts
|
|
123
123
|
var SDK_VERSION = version;
|
|
@@ -384,6 +384,7 @@ async function packageMacOS(config, outDir) {
|
|
|
384
384
|
iconPath: config.iconPath,
|
|
385
385
|
provisioningProfile: opts.provisioningProfile,
|
|
386
386
|
additionalExecutables: opts.additionalExecutables,
|
|
387
|
+
nativeRuntimePath: opts.nativeRuntimePath,
|
|
387
388
|
outputPath: appBundlePath
|
|
388
389
|
});
|
|
389
390
|
if (!appBundle.success) {
|
|
@@ -666,7 +667,7 @@ ${schemes.length ? `MimeType=${schemes.map((scheme) => `x-scheme-handler/${schem
|
|
|
666
667
|
}
|
|
667
668
|
function macOSInfoPlist(metadata) {
|
|
668
669
|
const entries = [
|
|
669
|
-
["CFBundleExecutable", metadata.name],
|
|
670
|
+
["CFBundleExecutable", metadata.executableName || metadata.name],
|
|
670
671
|
["CFBundleIdentifier", metadata.bundleId],
|
|
671
672
|
["CFBundleName", metadata.name],
|
|
672
673
|
["CFBundleShortVersionString", metadata.version],
|
|
@@ -809,6 +810,9 @@ function copyMacOSAppBundle(source, destination) {
|
|
|
809
810
|
return { success: false, error: error.message };
|
|
810
811
|
}
|
|
811
812
|
}
|
|
813
|
+
function macOSRuntimeExecutablePath(appBundlePath) {
|
|
814
|
+
return join2(appBundlePath, "Contents", "Helpers", "CraftRuntime.app", "Contents", "MacOS", "craft");
|
|
815
|
+
}
|
|
812
816
|
function createMacOSAppBundle(opts) {
|
|
813
817
|
try {
|
|
814
818
|
const { name, binaryPath, iconPath, provisioningProfile, additionalExecutables, outputPath } = opts;
|
|
@@ -825,6 +829,27 @@ function createMacOSAppBundle(opts) {
|
|
|
825
829
|
copyFileSync(executable, destination);
|
|
826
830
|
chmodSync(destination, 493);
|
|
827
831
|
}
|
|
832
|
+
if (opts.nativeRuntimePath) {
|
|
833
|
+
const helper = macOSRuntimeExecutablePath(outputPath);
|
|
834
|
+
const helperContents = resolve(helper, "..", "..");
|
|
835
|
+
mkdirSync(join2(helperContents, "MacOS"), { recursive: true });
|
|
836
|
+
copyFileSync(opts.nativeRuntimePath, helper);
|
|
837
|
+
chmodSync(helper, 493);
|
|
838
|
+
if (iconPath) {
|
|
839
|
+
mkdirSync(join2(helperContents, "Resources"), { recursive: true });
|
|
840
|
+
copyFileSync(iconPath, join2(helperContents, "Resources", "icon.icns"));
|
|
841
|
+
}
|
|
842
|
+
writeFileSync2(join2(helperContents, "Info.plist"), macOSInfoPlist({
|
|
843
|
+
name,
|
|
844
|
+
executableName: "craft",
|
|
845
|
+
version: opts.version,
|
|
846
|
+
bundleId: `${opts.bundleId}.runtime`,
|
|
847
|
+
buildNumber: opts.buildNumber,
|
|
848
|
+
minimumSystemVersion: opts.minimumSystemVersion,
|
|
849
|
+
menuBarOnly: true,
|
|
850
|
+
iconName: iconPath ? "icon" : undefined
|
|
851
|
+
}));
|
|
852
|
+
}
|
|
828
853
|
let iconName;
|
|
829
854
|
if (iconPath) {
|
|
830
855
|
if (!existsSync3(iconPath))
|
|
@@ -11595,6 +11620,7 @@ export {
|
|
|
11595
11620
|
liveActivities,
|
|
11596
11621
|
loadURL,
|
|
11597
11622
|
location,
|
|
11623
|
+
macOSRuntimeExecutablePath,
|
|
11598
11624
|
materialYou,
|
|
11599
11625
|
media,
|
|
11600
11626
|
menuItem,
|
package/dist/ios/src/index.js
CHANGED
|
@@ -189,7 +189,12 @@ function serializeSimulatorSteps(steps) {
|
|
|
189
189
|
};
|
|
190
190
|
const line = (step) => {
|
|
191
191
|
let arg = "";
|
|
192
|
-
if ("
|
|
192
|
+
if (step.command === "holdtab") {
|
|
193
|
+
const seconds = step.seconds ?? 6;
|
|
194
|
+
if (!step.text.trim() || step.text.includes("|") || number(seconds) < 2 || seconds > 10)
|
|
195
|
+
throw new Error("holdtab needs a nonempty tab name and 2\u201310 seconds");
|
|
196
|
+
arg = `${step.text} | ${seconds}`;
|
|
197
|
+
} else if ("paths" in step) {
|
|
193
198
|
if (!step.paths.length || step.paths.length > 64)
|
|
194
199
|
throw new Error("Expected 1\u201364 touch paths");
|
|
195
200
|
for (const path of step.paths) {
|
|
@@ -61,6 +61,12 @@ export type SimulatorStep = {
|
|
|
61
61
|
| {
|
|
62
62
|
command: 'touches';
|
|
63
63
|
paths: TimedTouchPoint[][];
|
|
64
|
+
}
|
|
65
|
+
/** Standard XCTest tab hold; independent screenshots must be captured before release. */
|
|
66
|
+
| {
|
|
67
|
+
command: 'holdtab';
|
|
68
|
+
text: string;
|
|
69
|
+
seconds?: number;
|
|
64
70
|
} | {
|
|
65
71
|
command: 'field';
|
|
66
72
|
index: number;
|
|
@@ -766,6 +766,13 @@ final class CraftChrome: ObservableObject {
|
|
|
766
766
|
@Published var tabAccessory: Tab?
|
|
767
767
|
private weak var tabAccessoryOwner: AnyObject?
|
|
768
768
|
@Published var selected: String?
|
|
769
|
+
/// The visible destination may be the accessory, independently of its owning stack.
|
|
770
|
+
@Published var visiblePath: String?
|
|
771
|
+
var accessorySelected: Bool {
|
|
772
|
+
guard let path = visiblePath, let accessory = tabAccessory?.id else { return false }
|
|
773
|
+
return path == accessory || path.hasPrefix(accessory + "/")
|
|
774
|
+
}
|
|
775
|
+
var highlightedTab: String? { accessorySelected ? nil : selected }
|
|
769
776
|
let tabSwipeProgress = CraftTabSwipeProgress()
|
|
770
777
|
private(set) var tabSwipePosition: CGFloat? {
|
|
771
778
|
get { tabSwipeProgress.position }
|
|
@@ -1172,6 +1179,18 @@ final class CraftChromeRelay: NSObject, WKScriptMessageHandler {
|
|
|
1172
1179
|
|
|
1173
1180
|
/// The floating tab bar: Liquid Glass on iOS 26 and later, a material capsule before.
|
|
1174
1181
|
struct CraftTabBarView: View {
|
|
1182
|
+
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
|
1183
|
+
|
|
1184
|
+
var body: some View {
|
|
1185
|
+
// ScaledMetric reads the environment of its owning view. Put the
|
|
1186
|
+
// metrics in a child so this compact-chrome cap reaches them too.
|
|
1187
|
+
CraftTabBarContent(viewerTextSize: dynamicTypeSize)
|
|
1188
|
+
.dynamicTypeSize(...DynamicTypeSize.xxxLarge)
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
private struct CraftTabBarContent: View {
|
|
1193
|
+
let viewerTextSize: DynamicTypeSize
|
|
1175
1194
|
@ObservedObject private var chrome = CraftChrome.shared
|
|
1176
1195
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
1177
1196
|
@Environment(\.scenePhase) private var scenePhase
|
|
@@ -1183,7 +1202,7 @@ struct CraftTabBarView: View {
|
|
|
1183
1202
|
@ScaledMetric(relativeTo: .caption2) private var badgeSize: CGFloat = 10
|
|
1184
1203
|
|
|
1185
1204
|
var body: some View {
|
|
1186
|
-
if
|
|
1205
|
+
if !chrome.tabs.isEmpty {
|
|
1187
1206
|
HStack(spacing: 8) {
|
|
1188
1207
|
bar
|
|
1189
1208
|
if let accessory = chrome.tabAccessory {
|
|
@@ -1191,8 +1210,13 @@ struct CraftTabBarView: View {
|
|
|
1191
1210
|
surface {
|
|
1192
1211
|
Image(systemName: accessory.symbol)
|
|
1193
1212
|
.font(.system(size: iconSize))
|
|
1194
|
-
.foregroundStyle(Color.primary)
|
|
1213
|
+
.foregroundStyle(chrome.accessorySelected ? selectedColor : Color.primary)
|
|
1195
1214
|
.frame(width: 62, height: 62)
|
|
1215
|
+
.background {
|
|
1216
|
+
if chrome.accessorySelected {
|
|
1217
|
+
Circle().fill(selectedColor.opacity(0.14)).padding(4)
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1196
1220
|
}
|
|
1197
1221
|
.contentShape(Rectangle())
|
|
1198
1222
|
.overlay(alignment: .topTrailing) {
|
|
@@ -1207,9 +1231,17 @@ struct CraftTabBarView: View {
|
|
|
1207
1231
|
}
|
|
1208
1232
|
}
|
|
1209
1233
|
.buttonStyle(.plain)
|
|
1234
|
+
.accessibilityShowsLargeContentViewer {
|
|
1235
|
+
Image(systemName: accessory.symbol)
|
|
1236
|
+
Text(accessory.accessibilityLabel ?? accessory.title)
|
|
1237
|
+
}
|
|
1238
|
+
// Keep accessibility intent for the viewer; displayed
|
|
1239
|
+
// sizes come from the bounded metrics owned above.
|
|
1240
|
+
.environment(\.dynamicTypeSize, viewerTextSize)
|
|
1210
1241
|
.accessibilityLabel(accessory.accessibilityLabel ?? accessory.title)
|
|
1211
1242
|
.accessibilityValue(accessory.badge ?? "")
|
|
1212
1243
|
.accessibilityIdentifier("craft-tab-accessory")
|
|
1244
|
+
.accessibilityAddTraits(chrome.accessorySelected ? .isSelected : [])
|
|
1213
1245
|
}
|
|
1214
1246
|
}
|
|
1215
1247
|
.padding(.horizontal, 16)
|
|
@@ -1219,9 +1251,14 @@ struct CraftTabBarView: View {
|
|
|
1219
1251
|
.onAppear { report(proxy) }
|
|
1220
1252
|
.onChange(of: proxy.frame(in: .global)) { _ in report(proxy) }
|
|
1221
1253
|
})
|
|
1222
|
-
|
|
1254
|
+
// Keep the view mounted: a disappearing conditional inside a
|
|
1255
|
+
// child does not reliably animate in the UIKit navigation transaction.
|
|
1256
|
+
.offset(y: chrome.tabBarVisible || reduceMotion ? 0 : 120)
|
|
1257
|
+
.opacity(chrome.tabBarVisible ? 1 : 0)
|
|
1258
|
+
.allowsHitTesting(chrome.tabBarVisible)
|
|
1259
|
+
.accessibilityHidden(!chrome.tabBarVisible)
|
|
1260
|
+
.animation(.easeInOut(duration: reduceMotion ? 0.12 : 0.24), value: chrome.tabBarVisible)
|
|
1223
1261
|
.ignoresSafeArea(.keyboard)
|
|
1224
|
-
.dynamicTypeSize(...DynamicTypeSize.xxxLarge)
|
|
1225
1262
|
}
|
|
1226
1263
|
}
|
|
1227
1264
|
|
|
@@ -1260,7 +1297,7 @@ struct CraftTabBarView: View {
|
|
|
1260
1297
|
private var items: some View {
|
|
1261
1298
|
HStack(spacing: 0) {
|
|
1262
1299
|
ForEach(chrome.tabs) { tab in
|
|
1263
|
-
let isSelected = tab.id == chrome.
|
|
1300
|
+
let isSelected = tab.id == chrome.highlightedTab
|
|
1264
1301
|
Button { chrome.tap(tab) } label: {
|
|
1265
1302
|
VStack(spacing: 3) {
|
|
1266
1303
|
ZStack(alignment: .topTrailing) {
|
|
@@ -1305,6 +1342,7 @@ struct CraftTabBarView: View {
|
|
|
1305
1342
|
Image(systemName: symbol(tab.symbol, selected: isSelected))
|
|
1306
1343
|
Text(tab.displayTitle)
|
|
1307
1344
|
}
|
|
1345
|
+
.environment(\.dynamicTypeSize, viewerTextSize)
|
|
1308
1346
|
.accessibilityIdentifier("craft-tab-\(tab.id)")
|
|
1309
1347
|
.accessibilityLabel(tab.accessibilityLabel ?? tab.badge.map { "\(tab.displayTitle), \($0)" } ?? tab.displayTitle)
|
|
1310
1348
|
.accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : .isButton)
|
|
@@ -1312,7 +1350,7 @@ struct CraftTabBarView: View {
|
|
|
1312
1350
|
}
|
|
1313
1351
|
.background {
|
|
1314
1352
|
CraftTabSelectionIndicator(
|
|
1315
|
-
selected: chrome.tabs.firstIndex(where: { $0.id == chrome.
|
|
1353
|
+
selected: chrome.tabs.firstIndex(where: { $0.id == chrome.highlightedTab }),
|
|
1316
1354
|
count: chrome.tabs.count,
|
|
1317
1355
|
cornerRadius: chrome.tabSelectionCornerRadius
|
|
1318
1356
|
)
|
|
@@ -1664,6 +1664,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1664
1664
|
(nav.topViewController as? CraftNativeScreenController)?.screenVisibilityChanged(true)
|
|
1665
1665
|
}
|
|
1666
1666
|
currentTab = id
|
|
1667
|
+
if let top = nav.topViewController { CraftChrome.shared.visiblePath = nativePaths[ObjectIdentifier(top)] }
|
|
1667
1668
|
for (key, other) in stacks { other.view.isHidden = key != id }
|
|
1668
1669
|
nav.view.frame = view.bounds
|
|
1669
1670
|
view.bringSubviewToFront(nav.view)
|
|
@@ -2042,6 +2043,9 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
2042
2043
|
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
|
|
2043
2044
|
lastNavigation = CACurrentMediaTime()
|
|
2044
2045
|
if animated { CraftFramePacing.shared.begin("navigation") }
|
|
2046
|
+
if navigationController === visibleStack {
|
|
2047
|
+
CraftChrome.shared.visiblePath = nativePaths[ObjectIdentifier(viewController)]
|
|
2048
|
+
}
|
|
2045
2049
|
// The page draws its own header, and a tab's root screen its own
|
|
2046
2050
|
// title unless it asked for the bar; a pushed native screen gets the
|
|
2047
2051
|
// bar and its Back.
|
|
@@ -2084,6 +2088,8 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
2084
2088
|
hybrid.pageReset(to: rootPath)
|
|
2085
2089
|
}
|
|
2086
2090
|
guard navigationController === visibleStack else { return }
|
|
2091
|
+
// Also restore selection after a cancelled interactive pop.
|
|
2092
|
+
CraftChrome.shared.visiblePath = nativePaths[ObjectIdentifier(viewController)]
|
|
2087
2093
|
if let slot = viewController as? CraftHybridWebSlot { adoptWeb(into: slot) } else { parkWeb() }
|
|
2088
2094
|
}
|
|
2089
2095
|
|
|
@@ -43,6 +43,7 @@ enum CraftNativeActions {
|
|
|
43
43
|
// `speech`, `audio` (background sound) and `eventStream` (a fetch read
|
|
44
44
|
// as it arrives) need nothing the app has to declare.
|
|
45
45
|
var values = ["device", "clipboard", "haptics", "storage", "lifecycle", "fetch", "dialog", "speech", "audio", "eventStream", "linking", "badge", "animation", "viewFocus", "viewMeasure", "viewReparent", "viewNonModalPresentation", "nativePlatformControls"]
|
|
46
|
+
values.append("nativePager")
|
|
46
47
|
values.append("storageCompareAndSet")
|
|
47
48
|
values.append("storageSetIfAbsent")
|
|
48
49
|
values.append("viewScroll")
|
|
@@ -2744,6 +2744,7 @@ final class CraftNativeCelebration {
|
|
|
2744
2744
|
}
|
|
2745
2745
|
|
|
2746
2746
|
private class CraftNativeFlowView: UIStackView {
|
|
2747
|
+
var pageViewport: CGSize? { didSet { if pageViewport != oldValue { invalidateMeasurements() } } }
|
|
2747
2748
|
var celebration: CraftNativeCelebration?
|
|
2748
2749
|
func configureCelebration(_ raw: Any?) {
|
|
2749
2750
|
if raw != nil && celebration == nil { celebration = CraftNativeCelebration(view: self) }
|
|
@@ -2966,6 +2967,7 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
2966
2967
|
}
|
|
2967
2968
|
|
|
2968
2969
|
func measure(width: CGFloat?, height: CGFloat?, maxWidth: CGFloat, maxHeight: CGFloat) -> CGSize {
|
|
2970
|
+
if let page = pageViewport { return CGSize(width: page.width * CGFloat(arrangedSubviews.filter { !$0.isHidden }.count), height: page.height) }
|
|
2969
2971
|
let key = MeasureKey(width: width, height: height, maxWidth: maxWidth, maxHeight: maxHeight)
|
|
2970
2972
|
if let cached = measurements[key] { return cached }
|
|
2971
2973
|
let size = layout(width: width, height: height, maxWidth: maxWidth, maxHeight: maxHeight, place: false).size
|
|
@@ -2977,6 +2979,12 @@ private class CraftNativeFlowView: UIStackView {
|
|
|
2977
2979
|
super.layoutSubviews()
|
|
2978
2980
|
craftApplyCornerRadius()
|
|
2979
2981
|
celebration?.layout()
|
|
2982
|
+
if let page = pageViewport {
|
|
2983
|
+
for (index, child) in arrangedSubviews.filter({ !$0.isHidden }).enumerated() {
|
|
2984
|
+
child.frame = CGRect(x: CGFloat(index) * page.width, y: 0, width: page.width, height: page.height)
|
|
2985
|
+
}
|
|
2986
|
+
return
|
|
2987
|
+
}
|
|
2980
2988
|
let scale = window?.screen.scale ?? traitCollection.displayScale
|
|
2981
2989
|
let result = layout(width: bounds.width, height: bounds.height, maxWidth: bounds.width, maxHeight: bounds.height, place: true)
|
|
2982
2990
|
var moved = false
|
|
@@ -4344,6 +4352,46 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
4344
4352
|
private var laidOutSize: CGSize = .zero
|
|
4345
4353
|
private let pullToRefresh = UIRefreshControl()
|
|
4346
4354
|
private var refreshHandler: (() -> Void)?
|
|
4355
|
+
private var authoredPageIndex: Int?
|
|
4356
|
+
private var pendingPageIndex: Int?
|
|
4357
|
+
private var currentPageIndex = 0
|
|
4358
|
+
private var pageAnimating = false
|
|
4359
|
+
private var reportedPage: (Int, Int)?
|
|
4360
|
+
private var pageHandler: ((Int, Int) -> Void)?
|
|
4361
|
+
|
|
4362
|
+
/// Opt-in viewport-sized pages. Ordinary horizontal scroll strips retain
|
|
4363
|
+
/// their intrinsic widths; only a controlled pageIndex enables this mode.
|
|
4364
|
+
func configurePager(_ raw: Any?, onSelect: ((Int, Int) -> Void)?) {
|
|
4365
|
+
let next = craftNativeNumber(raw).flatMap { number -> Int? in
|
|
4366
|
+
let value = number.doubleValue
|
|
4367
|
+
return value.isFinite ? Int(min(Double(Int.max / 2), max(0, value))) : nil
|
|
4368
|
+
}
|
|
4369
|
+
if next != authoredPageIndex { pendingPageIndex = next; contentNeedsLayout = true }
|
|
4370
|
+
authoredPageIndex = next
|
|
4371
|
+
pageHandler = onSelect
|
|
4372
|
+
if next == nil { contentStack.pageViewport = nil; reportedPage = nil; pageAnimating = false }
|
|
4373
|
+
setNeedsLayout()
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
func beginPageDrag() { pageAnimating = false }
|
|
4377
|
+
|
|
4378
|
+
func finishPageMotion() {
|
|
4379
|
+
pageAnimating = false
|
|
4380
|
+
DispatchQueue.main.async { [weak self] in self?.settlePage() }
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
func settlePage() {
|
|
4384
|
+
guard authoredPageIndex != nil, bounds.width > 0, !pageAnimating,
|
|
4385
|
+
!isTracking, !isDragging, !isDecelerating else { return }
|
|
4386
|
+
let pages = contentStack.arrangedSubviews.filter { !$0.isHidden }
|
|
4387
|
+
let index = min(max(0, Int((contentOffset.x / bounds.width).rounded())), max(0, pages.count - 1))
|
|
4388
|
+
currentPageIndex = index
|
|
4389
|
+
for (i, child) in pages.enumerated() { child.accessibilityElementsHidden = i != index }
|
|
4390
|
+
if reportedPage?.0 != index || reportedPage?.1 != pages.count {
|
|
4391
|
+
reportedPage = (index, pages.count)
|
|
4392
|
+
pageHandler?(index, pages.count)
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4347
4395
|
|
|
4348
4396
|
override init(frame: CGRect) {
|
|
4349
4397
|
super.init(frame: frame)
|
|
@@ -4384,6 +4432,11 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
4384
4432
|
/// The size of the content laid out across the visible width (or, sideways,
|
|
4385
4433
|
/// height), which is also the scroll view's own size when nothing fixes it.
|
|
4386
4434
|
func contentFitting(width: CGFloat?, height: CGFloat?) -> CGSize {
|
|
4435
|
+
if authoredPageIndex != nil && !isVertical {
|
|
4436
|
+
let pageWidth = max(0, width ?? bounds.width)
|
|
4437
|
+
let pageHeight = max(0, height ?? bounds.height)
|
|
4438
|
+
return CGSize(width: pageWidth * CGFloat(contentStack.arrangedSubviews.filter { !$0.isHidden }.count), height: pageHeight)
|
|
4439
|
+
}
|
|
4387
4440
|
if isVertical {
|
|
4388
4441
|
let contentWidth = max(0, (width ?? bounds.width) - adjustedContentInset.left - adjustedContentInset.right)
|
|
4389
4442
|
return contentStack.measure(width: contentWidth, height: nil, maxWidth: contentWidth, maxHeight: CraftNativeFlexLayout.unbounded)
|
|
@@ -4406,7 +4459,9 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
4406
4459
|
craftApplyCornerRadius()
|
|
4407
4460
|
if contentNeedsLayout || laidOutSize != bounds.size {
|
|
4408
4461
|
contentNeedsLayout = false
|
|
4462
|
+
let resized = laidOutSize != bounds.size
|
|
4409
4463
|
laidOutSize = bounds.size
|
|
4464
|
+
if authoredPageIndex != nil && !isVertical { contentStack.pageViewport = bounds.size }
|
|
4410
4465
|
var size = contentFitting(width: bounds.width, height: bounds.height)
|
|
4411
4466
|
if isVertical {
|
|
4412
4467
|
size.width = max(0, bounds.width - adjustedContentInset.left - adjustedContentInset.right)
|
|
@@ -4416,12 +4471,31 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
4416
4471
|
let frame = CGRect(origin: .zero, size: size)
|
|
4417
4472
|
if contentStack.frame != frame { contentStack.frame = frame }
|
|
4418
4473
|
if contentSize != size { contentSize = size }
|
|
4474
|
+
if authoredPageIndex != nil && !isVertical {
|
|
4475
|
+
contentStack.layoutIfNeeded()
|
|
4476
|
+
let count = contentStack.arrangedSubviews.filter { !$0.isHidden }.count
|
|
4477
|
+
let index = min(max(0, pendingPageIndex ?? currentPageIndex), max(0, count - 1))
|
|
4478
|
+
let target = CGPoint(x: CGFloat(index) * bounds.width, y: 0)
|
|
4479
|
+
let moves = pendingPageIndex != nil || resized || index != currentPageIndex
|
|
4480
|
+
let animated = moves && target != contentOffset && !resized && !UIAccessibility.isReduceMotionEnabled
|
|
4481
|
+
if moves {
|
|
4482
|
+
pageAnimating = animated
|
|
4483
|
+
setContentOffset(target, animated: animated)
|
|
4484
|
+
}
|
|
4485
|
+
pendingPageIndex = nil
|
|
4486
|
+
currentPageIndex = index
|
|
4487
|
+
if !animated && !isTracking && !isDragging && !isDecelerating { settlePage() }
|
|
4488
|
+
}
|
|
4419
4489
|
}
|
|
4420
4490
|
// A scroll view lays out on every frame of a scroll: the sticky
|
|
4421
4491
|
// headers follow it here, over content placed by this same pass.
|
|
4422
4492
|
if !stickyIndices.isEmpty || pendingScrollTarget != nil { contentStack.layoutIfNeeded() }
|
|
4423
4493
|
updateStickyHeaders()
|
|
4424
4494
|
scrollToPendingTarget()
|
|
4495
|
+
// A nonanimated target changes the offset synchronously inside this
|
|
4496
|
+
// layout pass. Rebase the headers against that actual viewport, not
|
|
4497
|
+
// the viewport from before the target was applied.
|
|
4498
|
+
updateStickyHeaders()
|
|
4425
4499
|
}
|
|
4426
4500
|
|
|
4427
4501
|
// MARK: Sticky headers
|
|
@@ -4444,7 +4518,7 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
4444
4518
|
view.center.y - view.bounds.height / 2
|
|
4445
4519
|
}
|
|
4446
4520
|
|
|
4447
|
-
|
|
4521
|
+
fileprivate func updateStickyHeaders() {
|
|
4448
4522
|
let children = contentStack.arrangedSubviews
|
|
4449
4523
|
let headers = isVertical ? stickyIndices.compactMap { $0 < children.count ? children[$0] : nil }.filter { !$0.isHidden } : []
|
|
4450
4524
|
for view in stickyViews where !headers.contains(where: { $0 === view }) {
|
|
@@ -4617,11 +4691,13 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
4617
4691
|
craftScrollGesture(gestureRecognizer, runsWith: otherGestureRecognizer)
|
|
4618
4692
|
}
|
|
4619
4693
|
|
|
4620
|
-
func setAxis(_ axis: NSLayoutConstraint.Axis) {
|
|
4694
|
+
func setAxis(_ axis: NSLayoutConstraint.Axis, resetBounce: Bool = true) {
|
|
4621
4695
|
if contentStack.axis != axis { setContentNeedsLayout() }
|
|
4622
4696
|
contentStack.axis = axis
|
|
4623
|
-
|
|
4624
|
-
|
|
4697
|
+
if resetBounce {
|
|
4698
|
+
alwaysBounceVertical = axis == .vertical
|
|
4699
|
+
alwaysBounceHorizontal = axis == .horizontal
|
|
4700
|
+
}
|
|
4625
4701
|
}
|
|
4626
4702
|
}
|
|
4627
4703
|
|
|
@@ -8461,6 +8537,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8461
8537
|
scroll.isScrollEnabled = props["scrollEnabled"] as? Bool != false
|
|
8462
8538
|
scroll.bounces = props["bounces"] as? Bool ?? true
|
|
8463
8539
|
scroll.isPagingEnabled = props["pagingEnabled"] as? Bool ?? false
|
|
8540
|
+
let pageHandler = nonEmptyHandler(events["onPageSelected"])
|
|
8541
|
+
scroll.configurePager(props["pageIndex"], onSelect: pageHandler.map { handler in
|
|
8542
|
+
{ [weak self] index, count in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["index": index, "count": count]]) }
|
|
8543
|
+
})
|
|
8464
8544
|
scroll.keyboardDismissMode = keyboardDismissMode(props["keyboardDismissMode"])
|
|
8465
8545
|
scroll.setKeyboardShouldPersistTaps(props["keyboardShouldPersistTaps"])
|
|
8466
8546
|
scroll.showsVerticalScrollIndicator = props["showsVerticalScrollIndicator"] as? Bool != false
|
|
@@ -8492,6 +8572,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
8492
8572
|
contentStyle.merge(override) { _, next in next }
|
|
8493
8573
|
}
|
|
8494
8574
|
configureStack(scroll.contentStack, style: contentStyle)
|
|
8575
|
+
// Content-style defaults are vertical; the scroll host's explicit
|
|
8576
|
+
// axis must survive that shared container configuration without
|
|
8577
|
+
// resetting the authored bounce options applied above.
|
|
8578
|
+
scroll.setAxis(direction, resetBounce: false)
|
|
8495
8579
|
reconcileChildren(children.filter {
|
|
8496
8580
|
guard let child = $0 as? [String: Any] else { return true }
|
|
8497
8581
|
return canonicalType(child["type"] as? String ?? "") != "RefreshControl"
|
|
@@ -9712,10 +9796,28 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9712
9796
|
for: size,
|
|
9713
9797
|
compatibleWith: traitCollection
|
|
9714
9798
|
)
|
|
9715
|
-
|
|
9716
|
-
|
|
9799
|
+
let families = (style["fontFamily"] as? String ?? "").split(separator: ",").map {
|
|
9800
|
+
$0.trimmingCharacters(in: .whitespacesAndNewlines).trimmingCharacters(in: CharacterSet(charactersIn: "\"'"))
|
|
9801
|
+
}
|
|
9802
|
+
let selectedFont = families.lazy.compactMap { family -> UIFont? in
|
|
9803
|
+
switch family.lowercased() {
|
|
9804
|
+
case "monospace", "ui-monospace": return .monospacedSystemFont(ofSize: scaledSize, weight: weight)
|
|
9805
|
+
case "serif", "ui-serif":
|
|
9806
|
+
let system = UIFont.systemFont(ofSize: scaledSize, weight: weight)
|
|
9807
|
+
return system.fontDescriptor.withDesign(.serif).map { UIFont(descriptor: $0, size: scaledSize) }
|
|
9808
|
+
case "sans-serif", "ui-sans-serif", "system-ui", "-apple-system": return .systemFont(ofSize: scaledSize, weight: weight)
|
|
9809
|
+
default:
|
|
9810
|
+
guard let custom = UIFont(name: family, size: scaledSize) else { return nil }
|
|
9811
|
+
let weighted = custom.fontDescriptor.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: weight.rawValue]])
|
|
9812
|
+
var traits = custom.fontDescriptor.symbolicTraits
|
|
9813
|
+
if weight >= .semibold { traits.insert(.traitBold) } else { traits.remove(.traitBold) }
|
|
9814
|
+
let descriptor = weighted.withSymbolicTraits(traits) ?? weighted
|
|
9815
|
+
return UIFont(descriptor: descriptor, size: scaledSize)
|
|
9816
|
+
}
|
|
9817
|
+
}.first
|
|
9818
|
+
var font = selectedFont ?? .systemFont(ofSize: scaledSize, weight: weight)
|
|
9717
9819
|
if style["fontStyle"] as? String == "italic",
|
|
9718
|
-
let descriptor = font.fontDescriptor.withSymbolicTraits(.traitItalic) {
|
|
9820
|
+
let descriptor = font.fontDescriptor.withSymbolicTraits(font.fontDescriptor.symbolicTraits.union(.traitItalic)) {
|
|
9719
9821
|
font = UIFont(descriptor: descriptor, size: scaledSize)
|
|
9720
9822
|
}
|
|
9721
9823
|
return font
|
|
@@ -10502,6 +10604,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10502
10604
|
}
|
|
10503
10605
|
|
|
10504
10606
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
10607
|
+
// Sticky positioning is a native scroll behavior and must also run
|
|
10608
|
+
// without a JavaScript onScroll subscription, including animations.
|
|
10609
|
+
(scrollView as? CraftNativeScrollView)?.updateStickyHeaders()
|
|
10505
10610
|
guard let handler = scrollHandlers[ObjectIdentifier(scrollView)] else { return }
|
|
10506
10611
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
10507
10612
|
}
|
|
@@ -10510,12 +10615,13 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10510
10615
|
|
|
10511
10616
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
10512
10617
|
CraftFramePacing.shared.begin(pacingName)
|
|
10618
|
+
(scrollView as? CraftNativeScrollView)?.beginPageDrag()
|
|
10513
10619
|
guard let handler = scrollBeginHandlers[ObjectIdentifier(scrollView)] else { return }
|
|
10514
10620
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
10515
10621
|
}
|
|
10516
10622
|
|
|
10517
10623
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
10518
|
-
if !decelerate { CraftFramePacing.shared.end(pacingName) }
|
|
10624
|
+
if !decelerate { CraftFramePacing.shared.end(pacingName); (scrollView as? CraftNativeScrollView)?.finishPageMotion() }
|
|
10519
10625
|
guard let handler = scrollEndHandlers[ObjectIdentifier(scrollView)] else { return }
|
|
10520
10626
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
10521
10627
|
}
|
|
@@ -10527,10 +10633,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10527
10633
|
|
|
10528
10634
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
10529
10635
|
CraftFramePacing.shared.end(pacingName)
|
|
10636
|
+
(scrollView as? CraftNativeScrollView)?.finishPageMotion()
|
|
10530
10637
|
guard let handler = scrollMomentumEndHandlers[ObjectIdentifier(scrollView)] else { return }
|
|
10531
10638
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": scrollEvent(scrollView)])
|
|
10532
10639
|
}
|
|
10533
10640
|
|
|
10641
|
+
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
|
10642
|
+
(scrollView as? CraftNativeScrollView)?.finishPageMotion()
|
|
10643
|
+
}
|
|
10644
|
+
|
|
10534
10645
|
private func scrollEvent(_ scrollView: UIScrollView) -> [String: Any] {
|
|
10535
10646
|
[
|
|
10536
10647
|
"contentOffset": ["x": scrollView.contentOffset.x, "y": scrollView.contentOffset.y],
|
|
@@ -22,6 +22,7 @@ import Darwin
|
|
|
22
22
|
/// tapif <text> tap it when it is on screen, else carry on (the big timer's "Show the controls")
|
|
23
23
|
/// tapid <identifier>
|
|
24
24
|
/// longpressid <identifier> holds a hittable native element to open its edit menu
|
|
25
|
+
/// holdtab <name> [| secs] standard XCTest hold (2–10s, default 6), BEGIN/END timing receipt
|
|
25
26
|
/// touches <JSON> timed normalized touch paths in one native dispatch
|
|
26
27
|
/// tapxy <x> <y> normalised 0…1 of the screen
|
|
27
28
|
/// tapexact <label> a button whose label is exactly this
|
|
@@ -381,6 +382,43 @@ final class DriverTests: XCTestCase {
|
|
|
381
382
|
}
|
|
382
383
|
target.press(forDuration: 1.2)
|
|
383
384
|
note("longpressid \(arg): ok")
|
|
385
|
+
case "holdtab":
|
|
386
|
+
let parts = arg.components(separatedBy: "|").map { $0.trimmingCharacters(in: .whitespaces) }
|
|
387
|
+
guard parts.count <= 2, let name = parts.first, !name.isEmpty,
|
|
388
|
+
let duration = parts.count == 2 ? Double(parts[1]) : 6,
|
|
389
|
+
duration.isFinite, (2...10).contains(duration) else {
|
|
390
|
+
check(false, "holdtab needs a tab name and 2–10 seconds")
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
guard let button = poll(10, { tabButton(name) }) else {
|
|
394
|
+
check(false, "holdtab cannot find tab \(name)")
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
let frame = button.frame
|
|
398
|
+
let coordinate = button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
|
|
399
|
+
let startedAt = Date().timeIntervalSince1970
|
|
400
|
+
let startedUptime = ProcessInfo.processInfo.systemUptime
|
|
401
|
+
// BEGIN arms the independent screenshot watcher. XCTest still
|
|
402
|
+
// performs its normal idle checks before synthesizing the press;
|
|
403
|
+
// the watcher must wait for that synthesis, not infer touch-down.
|
|
404
|
+
note("BEGIN holdtab \(name) | \(duration) | \(startedAt)")
|
|
405
|
+
fflush(nil)
|
|
406
|
+
coordinate.press(forDuration: duration)
|
|
407
|
+
let returnedAt = Date().timeIntervalSince1970
|
|
408
|
+
let returnedUptime = ProcessInfo.processInfo.systemUptime
|
|
409
|
+
note("END holdtab \(name) | \(returnedAt)")
|
|
410
|
+
let receipt: [String: Any] = [
|
|
411
|
+
"tab": name, "requestedSeconds": duration,
|
|
412
|
+
"input": "XCUICoordinate.press(forDuration:)",
|
|
413
|
+
"startedAt": startedAt, "returnedAt": returnedAt,
|
|
414
|
+
"startedUptime": startedUptime, "returnedUptime": returnedUptime,
|
|
415
|
+
"frame": ["x": frame.minX, "y": frame.minY, "width": frame.width, "height": frame.height],
|
|
416
|
+
"note": "Call times include XCTest idle checks; during-hold screenshots require an independent synthesis-timed capture.",
|
|
417
|
+
]
|
|
418
|
+
if let data = try? JSONSerialization.data(withJSONObject: receipt, options: [.sortedKeys]) {
|
|
419
|
+
post("/save?name=holdtab-\(Int(startedUptime * 1000)).json", data)
|
|
420
|
+
}
|
|
421
|
+
check(true, "holdtab \(name): standard \(duration)s press returned")
|
|
384
422
|
case "touches":
|
|
385
423
|
do {
|
|
386
424
|
guard let data = arg.data(using: .utf8),
|
package/dist/package.d.ts
CHANGED
|
@@ -83,13 +83,17 @@ export interface PackageConfig {
|
|
|
83
83
|
applePassword?: string;
|
|
84
84
|
/** Team ID for notarization. Required by `notarytool` alongside `appleId`. */
|
|
85
85
|
teamId?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Native Craft provider for a server-backed app. Embeds a separately
|
|
88
|
+
* identified agent app so AppKit, Accessibility and MenuBarAgent resolve
|
|
89
|
+
* the GUI process to its own CFBundleExecutable.
|
|
90
|
+
*/
|
|
91
|
+
nativeRuntimePath?: string;
|
|
86
92
|
/**
|
|
87
93
|
* Extra executables copied into `Contents/MacOS/` beside the app binary.
|
|
88
94
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* self-contained. Resolve it relative to the running executable — on macOS
|
|
92
|
-
* the helper lands next to `process.execPath`.
|
|
95
|
+
* Use nativeRuntimePath for the GUI provider. This option is for non-GUI
|
|
96
|
+
* command line helpers, resolved beside process.execPath.
|
|
93
97
|
*/
|
|
94
98
|
additionalExecutables?: string[];
|
|
95
99
|
/**
|
|
@@ -173,6 +177,8 @@ export declare function formatPackagingCommandError(tool: string, code: number |
|
|
|
173
177
|
export declare function packageApp(config: PackageConfig): Promise<PackageResult[]>;
|
|
174
178
|
export interface MacOSBundleMetadata {
|
|
175
179
|
name: string;
|
|
180
|
+
/** Actual executable basename when it differs from the app's display name. */
|
|
181
|
+
executableName?: string;
|
|
176
182
|
version: string;
|
|
177
183
|
bundleId: string;
|
|
178
184
|
/** `CFBundleVersion`. Defaults to `version`. */
|
|
@@ -238,6 +244,11 @@ export declare function notarytoolArguments(opts: {
|
|
|
238
244
|
issuer: string;
|
|
239
245
|
};
|
|
240
246
|
}): string[];
|
|
247
|
+
/**
|
|
248
|
+
* Helper: Create macOS app bundle
|
|
249
|
+
*/
|
|
250
|
+
/** The native GUI agent's location, shared with desktop launchers. */
|
|
251
|
+
export declare function macOSRuntimeExecutablePath(appBundlePath: string): string;
|
|
241
252
|
export declare function dmgCapacityMegabytes(contentBytes: number): number;
|
|
242
253
|
export declare function dmgCreateArguments(opts: {
|
|
243
254
|
appBundlePath: string;
|
package/dist/types.d.ts
CHANGED
|
@@ -1187,20 +1187,18 @@ export interface CraftBridgeAPI {
|
|
|
1187
1187
|
tray?: CraftTrayAPI;
|
|
1188
1188
|
/** Native status-item arrangement and collapse controls (macOS). */
|
|
1189
1189
|
menubar?: {
|
|
1190
|
-
|
|
1190
|
+
/** Native visibility requires macOS 27, a direct build and Accessibility.
|
|
1191
|
+
* No automatic fallback to width-based hiding is performed. */
|
|
1192
|
+
init(options?: MenubarInitOptions): Promise<void>;
|
|
1193
|
+
destroy(): Promise<void>;
|
|
1194
|
+
requestPermission(): Promise<void>;
|
|
1191
1195
|
collapse(): Promise<void>;
|
|
1192
1196
|
expand(): Promise<void>;
|
|
1193
1197
|
toggle(): Promise<void>;
|
|
1194
|
-
getState(): Promise<
|
|
1195
|
-
collapsed: boolean;
|
|
1196
|
-
initialized: boolean;
|
|
1197
|
-
separatorHidden: boolean;
|
|
1198
|
-
}>;
|
|
1198
|
+
getState(): Promise<MenubarState>;
|
|
1199
1199
|
setAutoCollapse(seconds: number): Promise<void>;
|
|
1200
1200
|
setSeparatorHidden(hidden: boolean): Promise<void>;
|
|
1201
|
-
onStateChange(callback: (state:
|
|
1202
|
-
collapsed: boolean;
|
|
1203
|
-
}) => void): () => void;
|
|
1201
|
+
onStateChange(callback: (state: MenubarState) => void): () => void;
|
|
1204
1202
|
};
|
|
1205
1203
|
/**
|
|
1206
1204
|
* Window control (desktop)
|
|
@@ -2373,3 +2371,24 @@ export interface CraftTimersAPI {
|
|
|
2373
2371
|
signal?: AbortSignal;
|
|
2374
2372
|
}): Promise<void>;
|
|
2375
2373
|
}
|
|
2374
|
+
/** Native grouping policy. Unselected application icons are always protected. */
|
|
2375
|
+
export interface MenubarInitOptions {
|
|
2376
|
+
backend?: 'legacy' | 'nativeVisibility';
|
|
2377
|
+
/** Defaults to true. False accepts native suppression of Apple modules,
|
|
2378
|
+
* including Focus, Now Playing and camera/microphone indicators. */
|
|
2379
|
+
preserveSystemItems?: boolean;
|
|
2380
|
+
}
|
|
2381
|
+
/** Settled native menu-bar state; private visibility capability is explicit. */
|
|
2382
|
+
export interface MenubarState {
|
|
2383
|
+
collapsed: boolean;
|
|
2384
|
+
initialized: boolean;
|
|
2385
|
+
separatorHidden: boolean;
|
|
2386
|
+
backend?: 'legacy' | 'nativeVisibility';
|
|
2387
|
+
available?: boolean;
|
|
2388
|
+
authorized?: boolean;
|
|
2389
|
+
busy?: boolean;
|
|
2390
|
+
hiddenAppCount?: number;
|
|
2391
|
+
preserveSystemItems?: boolean;
|
|
2392
|
+
markerWidth?: number;
|
|
2393
|
+
error?: 'unavailable' | 'permissionRequired' | 'layoutUnavailable' | 'emptyGroup' | 'visibilityMismatch' | 'activationFailed' | 'activationTimeout' | null;
|
|
2394
|
+
}
|