craft-native 0.0.471 → 0.0.476
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/templates/CraftNativeShell.kt.template +46 -6
- package/dist/cli.js +21 -6
- package/dist/index.cjs +21 -6
- package/dist/index.js +21 -6
- package/dist/ios/templates/CraftApp.swift +50 -36
- package/dist/ios/templates/CraftHybrid.swift +43 -1
- package/dist/ios/templates/CraftNativeScreen.swift +48 -13
- package/dist/ios/templates/CraftNativeUI.swift +13 -5
- package/dist/package.d.ts +5 -0
- package/package.json +1 -1
|
@@ -844,6 +844,9 @@ internal class CraftNativeChrome(
|
|
|
844
844
|
private val tabBar = LinearLayout(context)
|
|
845
845
|
private val tabRow = LinearLayout(context)
|
|
846
846
|
private val accessory = FrameLayout(context)
|
|
847
|
+
private val companionRow = LinearLayout(context)
|
|
848
|
+
private val companionViews = mutableListOf<Pair<FrameLayout, JSONObject>>()
|
|
849
|
+
private var renderedCompanionSpecs = ""
|
|
847
850
|
private val accessoryIcon = ImageView(context)
|
|
848
851
|
private val accessoryBadge = TextView(context)
|
|
849
852
|
private var accessorySpec: JSONObject? = null
|
|
@@ -864,11 +867,16 @@ internal class CraftNativeChrome(
|
|
|
864
867
|
private var renderedButtonSpecs: Pair<String, String>? = null
|
|
865
868
|
private val density = resources.displayMetrics.density
|
|
866
869
|
private var screenBackgroundColor: Int? = null
|
|
870
|
+
private var navigationTitleColor: Int? = null
|
|
867
871
|
private var selectedTab = 0
|
|
868
872
|
private var tabBarHiddenByScreen = false
|
|
869
873
|
private var visiblePath: String? = null
|
|
870
874
|
private val accessorySelected: Boolean
|
|
871
|
-
get() = accessorySpec?.
|
|
875
|
+
get() = accessorySpec?.let { isAccessorySelected(it) } == true || companionViews.any { isAccessorySelected(it.second) }
|
|
876
|
+
private fun isAccessorySelected(item: JSONObject): Boolean {
|
|
877
|
+
val path = item.optString("path")
|
|
878
|
+
return visiblePath == path || visiblePath?.startsWith(path + "/") == true
|
|
879
|
+
}
|
|
872
880
|
private var swipeTracking = false
|
|
873
881
|
private var swipeIntercepted = false
|
|
874
882
|
private var horizontalScrollDeferred = false
|
|
@@ -965,6 +973,9 @@ internal class CraftNativeChrome(
|
|
|
965
973
|
tabRow.gravity = Gravity.CENTER_VERTICAL
|
|
966
974
|
tabRow.setPadding(dp(12), dp(4), dp(12), dp(4))
|
|
967
975
|
tabRow.addView(tabBar, LayoutParams(0, dp(64), 1f))
|
|
976
|
+
companionRow.orientation = HORIZONTAL
|
|
977
|
+
companionRow.gravity = Gravity.CENTER_VERTICAL
|
|
978
|
+
tabRow.addView(companionRow, LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, dp(64)))
|
|
968
979
|
accessory.addView(accessoryIcon, FrameLayout.LayoutParams(dp(26), dp(26), Gravity.CENTER))
|
|
969
980
|
accessoryBadge.textSize = 10f
|
|
970
981
|
accessoryBadge.gravity = Gravity.CENTER
|
|
@@ -1039,6 +1050,10 @@ internal class CraftNativeChrome(
|
|
|
1039
1050
|
?.takeUnless { it == JSONObject.NULL }
|
|
1040
1051
|
?.let { craftNativeOptionalString(it) }
|
|
1041
1052
|
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
1053
|
+
navigationTitleColor = options.opt("titleColor")
|
|
1054
|
+
?.takeUnless { it == JSONObject.NULL }
|
|
1055
|
+
?.let { craftNativeOptionalString(it) }
|
|
1056
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
1042
1057
|
options.optJSONObject("tabItem")?.let(::updateTab)
|
|
1043
1058
|
val hidden = options.optBoolean("hidden", false)
|
|
1044
1059
|
val large = options.optBoolean("largeTitle", false) && depth == 0
|
|
@@ -1087,6 +1102,24 @@ internal class CraftNativeChrome(
|
|
|
1087
1102
|
path.startsWith("/") && !path.startsWith("//") && it.has("title")
|
|
1088
1103
|
}
|
|
1089
1104
|
accessory.visibility = if (accessorySpec == null) View.GONE else View.VISIBLE
|
|
1105
|
+
val companions = accessorySpec?.optJSONArray("companions")
|
|
1106
|
+
val companionSpecs = companions?.toString().orEmpty()
|
|
1107
|
+
if (companionSpecs != renderedCompanionSpecs) {
|
|
1108
|
+
renderedCompanionSpecs = companionSpecs
|
|
1109
|
+
companionRow.removeAllViews()
|
|
1110
|
+
companionViews.clear()
|
|
1111
|
+
for (index in 0 until minOf(companions?.length() ?: 0, 2)) {
|
|
1112
|
+
val item = companions?.optJSONObject(index) ?: continue
|
|
1113
|
+
val path = item.optString("path")
|
|
1114
|
+
if (!path.startsWith("/") || path.startsWith("//") || path == accessorySpec?.optString("path") || !item.has("title")) continue
|
|
1115
|
+
val action = FrameLayout(context)
|
|
1116
|
+
action.contentDescription = item.optString("accessibilityLabel").ifBlank { item.optString("title") }
|
|
1117
|
+
action.addView(ImageView(context), FrameLayout.LayoutParams(dp(26), dp(26), Gravity.CENTER))
|
|
1118
|
+
action.setOnClickListener { onAccessory?.invoke(path) }
|
|
1119
|
+
companionRow.addView(action, LayoutParams(dp(48), dp(48)).apply { marginStart = dp(8) })
|
|
1120
|
+
companionViews.add(action to item)
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1090
1123
|
accessorySpec?.let {
|
|
1091
1124
|
accessory.contentDescription = it.optString("accessibilityLabel").ifBlank { it.optString("title") } + it.optString("badge").takeIf { badge -> badge.isNotBlank() && badge != "null" }?.let { badge -> ", $badge" }.orEmpty()
|
|
1092
1125
|
accessoryBadge.text = it.optString("badge").takeUnless { badge -> badge == "null" }.orEmpty().take(8)
|
|
@@ -1186,14 +1219,21 @@ internal class CraftNativeChrome(
|
|
|
1186
1219
|
setBackgroundColor(background)
|
|
1187
1220
|
bar.setBackgroundColor(background)
|
|
1188
1221
|
largeTitleView.setBackgroundColor(background)
|
|
1189
|
-
titleView.setTextColor(text)
|
|
1190
|
-
largeTitleView.setTextColor(text)
|
|
1222
|
+
titleView.setTextColor(navigationTitleColor ?: text)
|
|
1223
|
+
largeTitleView.setTextColor(navigationTitleColor ?: text)
|
|
1191
1224
|
backButton.imageTintList = ColorStateList.valueOf(accent)
|
|
1192
1225
|
val surface = if (dark) Color.parseColor("#1C1C1E") else Color.parseColor("#F9F9F9")
|
|
1193
1226
|
tabBar.background = GradientDrawable().apply { setColor(surface); cornerRadius = dp(32).toFloat() }
|
|
1194
|
-
|
|
1195
|
-
accessory.
|
|
1196
|
-
|
|
1227
|
+
val primarySelected = accessorySpec?.let { isAccessorySelected(it) } == true
|
|
1228
|
+
accessory.isSelected = primarySelected
|
|
1229
|
+
companionViews.forEach { (action, item) ->
|
|
1230
|
+
val selected = isAccessorySelected(item)
|
|
1231
|
+
action.isSelected = selected
|
|
1232
|
+
action.background = GradientDrawable().apply { setColor(if (selected) Color.argb(36, Color.red(accent), Color.green(accent), Color.blue(accent)) else surface); cornerRadius = dp(32).toFloat() }
|
|
1233
|
+
(action.getChildAt(0) as ImageView).setImageDrawable(CraftNativeIcons.drawable(context, null, item.optString("symbol"), if (selected) accent else text))
|
|
1234
|
+
}
|
|
1235
|
+
accessory.background = GradientDrawable().apply { setColor(if (primarySelected) Color.argb(36, Color.red(accent), Color.green(accent), Color.blue(accent)) else surface); cornerRadius = dp(32).toFloat() }
|
|
1236
|
+
accessorySpec?.let { accessoryIcon.setImageDrawable(CraftNativeIcons.drawable(context, null, it.optString("symbol"), if (primarySelected) accent else text)) }
|
|
1197
1237
|
tabViews.forEachIndexed { index, (icon, label) ->
|
|
1198
1238
|
tabBar.getChildAt(index).isSelected = index == selectedTab && !accessorySelected
|
|
1199
1239
|
tabBar.getChildAt(index).background = tabCellBackground(index, dark)
|
package/dist/cli.js
CHANGED
|
@@ -1012,13 +1012,16 @@ async function detachDiskImage(mount, options = {}) {
|
|
|
1012
1012
|
const wait = options.wait ?? ((ms) => new Promise((resolve) => {
|
|
1013
1013
|
setTimeout(resolve, ms);
|
|
1014
1014
|
}));
|
|
1015
|
+
const attempts = [];
|
|
1015
1016
|
for (let attempt = 1;; attempt++) {
|
|
1016
1017
|
const forced = attempt >= HDIUTIL_MAX_ATTEMPTS;
|
|
1017
|
-
const
|
|
1018
|
+
const args = forced ? ["detach", mount, "-force"] : ["detach", mount];
|
|
1019
|
+
const result = await run(args);
|
|
1018
1020
|
if (result.success)
|
|
1019
1021
|
return { success: true };
|
|
1022
|
+
attempts.push({ args, error: result.error });
|
|
1020
1023
|
if (!shouldRetryHdiutil(result.error, attempt))
|
|
1021
|
-
return { success: false, error: result.error };
|
|
1024
|
+
return { success: false, error: result.error, attempts };
|
|
1022
1025
|
await wait(attempt * 2000);
|
|
1023
1026
|
}
|
|
1024
1027
|
}
|
|
@@ -1030,6 +1033,9 @@ async function createDiskImage(opts) {
|
|
|
1030
1033
|
const mount = join3(scratch, "mount");
|
|
1031
1034
|
const writable = join3(scratch, "installer.dmg");
|
|
1032
1035
|
let mounted = false;
|
|
1036
|
+
let phase = "stage";
|
|
1037
|
+
let attachment = "";
|
|
1038
|
+
let detachAttempts = [];
|
|
1033
1039
|
const run = async (tool, args) => {
|
|
1034
1040
|
const result = await runTool(tool, args);
|
|
1035
1041
|
if (!result.success)
|
|
@@ -1046,6 +1052,7 @@ async function createDiskImage(opts) {
|
|
|
1046
1052
|
writeFileSync4(join3(payload, ".background", "installer.png"), installerBackground);
|
|
1047
1053
|
const args = dmgCreateArguments({ ...opts, appBundlePath: payload, outputPath: writable }, pathContentBytes(payload));
|
|
1048
1054
|
args[args.indexOf("ULMO")] = "UDRW";
|
|
1055
|
+
phase = "create";
|
|
1049
1056
|
for (let attempt = 1;; attempt++) {
|
|
1050
1057
|
const result = await runHdiutil(args);
|
|
1051
1058
|
if (result.success)
|
|
@@ -1055,17 +1062,25 @@ async function createDiskImage(opts) {
|
|
|
1055
1062
|
rmSync2(writable, { force: true });
|
|
1056
1063
|
await new Promise((resolve) => setTimeout(resolve, attempt * 2000));
|
|
1057
1064
|
}
|
|
1058
|
-
|
|
1065
|
+
phase = "attach";
|
|
1066
|
+
attachment = (await run("hdiutil", ["attach", writable, "-nobrowse", "-mountpoint", mount])).stdout;
|
|
1059
1067
|
mounted = true;
|
|
1068
|
+
phase = "layout";
|
|
1060
1069
|
writeInstallerLayout(mount, opts.volumeName, bundleName);
|
|
1070
|
+
phase = "detach";
|
|
1061
1071
|
const detached = await detachDiskImage(mount);
|
|
1062
|
-
if (!detached.success)
|
|
1072
|
+
if (!detached.success) {
|
|
1073
|
+
detachAttempts = detached.attempts;
|
|
1063
1074
|
throw new Error(detached.error);
|
|
1075
|
+
}
|
|
1064
1076
|
mounted = false;
|
|
1077
|
+
phase = "convert";
|
|
1065
1078
|
await run("hdiutil", ["convert", writable, "-format", "ULMO", "-ov", "-o", opts.outputPath]);
|
|
1066
1079
|
return { success: true, outputPath: opts.outputPath };
|
|
1067
1080
|
} catch (error) {
|
|
1068
|
-
|
|
1081
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1082
|
+
return { success: false, error: `${message}
|
|
1083
|
+
Disk image lifecycle: ${JSON.stringify({ phase, image: writable, mount, attachment, detachAttempts })}` };
|
|
1069
1084
|
} finally {
|
|
1070
1085
|
if (mounted)
|
|
1071
1086
|
await runTool("hdiutil", ["detach", mount, "-force"]);
|
|
@@ -3228,7 +3243,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3228
3243
|
`);
|
|
3229
3244
|
}
|
|
3230
3245
|
// package.json
|
|
3231
|
-
var version = "0.0.
|
|
3246
|
+
var version = "0.0.476";
|
|
3232
3247
|
|
|
3233
3248
|
// bin/cli.ts
|
|
3234
3249
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
|
@@ -376,7 +376,7 @@ function craftBinaryNotFoundMessage(triedPath) {
|
|
|
376
376
|
`);
|
|
377
377
|
}
|
|
378
378
|
// package.json
|
|
379
|
-
var version = "0.0.
|
|
379
|
+
var version = "0.0.476";
|
|
380
380
|
|
|
381
381
|
// src/sdk-version.ts
|
|
382
382
|
var SDK_VERSION = version;
|
|
@@ -1380,13 +1380,16 @@ async function detachDiskImage(mount, options = {}) {
|
|
|
1380
1380
|
const wait = options.wait ?? ((ms) => new Promise((resolve) => {
|
|
1381
1381
|
setTimeout(resolve, ms);
|
|
1382
1382
|
}));
|
|
1383
|
+
const attempts = [];
|
|
1383
1384
|
for (let attempt = 1;; attempt++) {
|
|
1384
1385
|
const forced = attempt >= HDIUTIL_MAX_ATTEMPTS;
|
|
1385
|
-
const
|
|
1386
|
+
const args = forced ? ["detach", mount, "-force"] : ["detach", mount];
|
|
1387
|
+
const result = await run(args);
|
|
1386
1388
|
if (result.success)
|
|
1387
1389
|
return { success: true };
|
|
1390
|
+
attempts.push({ args, error: result.error });
|
|
1388
1391
|
if (!shouldRetryHdiutil(result.error, attempt))
|
|
1389
|
-
return { success: false, error: result.error };
|
|
1392
|
+
return { success: false, error: result.error, attempts };
|
|
1390
1393
|
await wait(attempt * 2000);
|
|
1391
1394
|
}
|
|
1392
1395
|
}
|
|
@@ -1398,6 +1401,9 @@ async function createDiskImage(opts) {
|
|
|
1398
1401
|
const mount = import_path.join(scratch, "mount");
|
|
1399
1402
|
const writable = import_path.join(scratch, "installer.dmg");
|
|
1400
1403
|
let mounted = false;
|
|
1404
|
+
let phase = "stage";
|
|
1405
|
+
let attachment = "";
|
|
1406
|
+
let detachAttempts = [];
|
|
1401
1407
|
const run = async (tool, args) => {
|
|
1402
1408
|
const result = await runTool(tool, args);
|
|
1403
1409
|
if (!result.success)
|
|
@@ -1414,6 +1420,7 @@ async function createDiskImage(opts) {
|
|
|
1414
1420
|
import_fs.writeFileSync(import_path.join(payload, ".background", "installer.png"), installerBackground);
|
|
1415
1421
|
const args = dmgCreateArguments({ ...opts, appBundlePath: payload, outputPath: writable }, pathContentBytes(payload));
|
|
1416
1422
|
args[args.indexOf("ULMO")] = "UDRW";
|
|
1423
|
+
phase = "create";
|
|
1417
1424
|
for (let attempt = 1;; attempt++) {
|
|
1418
1425
|
const result = await runHdiutil(args);
|
|
1419
1426
|
if (result.success)
|
|
@@ -1423,17 +1430,25 @@ async function createDiskImage(opts) {
|
|
|
1423
1430
|
import_fs.rmSync(writable, { force: true });
|
|
1424
1431
|
await new Promise((resolve) => setTimeout(resolve, attempt * 2000));
|
|
1425
1432
|
}
|
|
1426
|
-
|
|
1433
|
+
phase = "attach";
|
|
1434
|
+
attachment = (await run("hdiutil", ["attach", writable, "-nobrowse", "-mountpoint", mount])).stdout;
|
|
1427
1435
|
mounted = true;
|
|
1436
|
+
phase = "layout";
|
|
1428
1437
|
writeInstallerLayout(mount, opts.volumeName, bundleName);
|
|
1438
|
+
phase = "detach";
|
|
1429
1439
|
const detached = await detachDiskImage(mount);
|
|
1430
|
-
if (!detached.success)
|
|
1440
|
+
if (!detached.success) {
|
|
1441
|
+
detachAttempts = detached.attempts;
|
|
1431
1442
|
throw new Error(detached.error);
|
|
1443
|
+
}
|
|
1432
1444
|
mounted = false;
|
|
1445
|
+
phase = "convert";
|
|
1433
1446
|
await run("hdiutil", ["convert", writable, "-format", "ULMO", "-ov", "-o", opts.outputPath]);
|
|
1434
1447
|
return { success: true, outputPath: opts.outputPath };
|
|
1435
1448
|
} catch (error) {
|
|
1436
|
-
|
|
1449
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1450
|
+
return { success: false, error: `${message}
|
|
1451
|
+
Disk image lifecycle: ${JSON.stringify({ phase, image: writable, mount, attachment, detachAttempts })}` };
|
|
1437
1452
|
} finally {
|
|
1438
1453
|
if (mounted)
|
|
1439
1454
|
await runTool("hdiutil", ["detach", mount, "-force"]);
|
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.476";
|
|
121
121
|
|
|
122
122
|
// src/sdk-version.ts
|
|
123
123
|
var SDK_VERSION = version;
|
|
@@ -1136,13 +1136,16 @@ async function detachDiskImage(mount, options = {}) {
|
|
|
1136
1136
|
const wait = options.wait ?? ((ms) => new Promise((resolve) => {
|
|
1137
1137
|
setTimeout(resolve, ms);
|
|
1138
1138
|
}));
|
|
1139
|
+
const attempts = [];
|
|
1139
1140
|
for (let attempt = 1;; attempt++) {
|
|
1140
1141
|
const forced = attempt >= HDIUTIL_MAX_ATTEMPTS;
|
|
1141
|
-
const
|
|
1142
|
+
const args = forced ? ["detach", mount, "-force"] : ["detach", mount];
|
|
1143
|
+
const result = await run(args);
|
|
1142
1144
|
if (result.success)
|
|
1143
1145
|
return { success: true };
|
|
1146
|
+
attempts.push({ args, error: result.error });
|
|
1144
1147
|
if (!shouldRetryHdiutil(result.error, attempt))
|
|
1145
|
-
return { success: false, error: result.error };
|
|
1148
|
+
return { success: false, error: result.error, attempts };
|
|
1146
1149
|
await wait(attempt * 2000);
|
|
1147
1150
|
}
|
|
1148
1151
|
}
|
|
@@ -1154,6 +1157,9 @@ async function createDiskImage(opts) {
|
|
|
1154
1157
|
const mount = join3(scratch, "mount");
|
|
1155
1158
|
const writable = join3(scratch, "installer.dmg");
|
|
1156
1159
|
let mounted = false;
|
|
1160
|
+
let phase = "stage";
|
|
1161
|
+
let attachment = "";
|
|
1162
|
+
let detachAttempts = [];
|
|
1157
1163
|
const run = async (tool, args) => {
|
|
1158
1164
|
const result = await runTool(tool, args);
|
|
1159
1165
|
if (!result.success)
|
|
@@ -1170,6 +1176,7 @@ async function createDiskImage(opts) {
|
|
|
1170
1176
|
writeFileSync3(join3(payload, ".background", "installer.png"), installerBackground);
|
|
1171
1177
|
const args = dmgCreateArguments({ ...opts, appBundlePath: payload, outputPath: writable }, pathContentBytes(payload));
|
|
1172
1178
|
args[args.indexOf("ULMO")] = "UDRW";
|
|
1179
|
+
phase = "create";
|
|
1173
1180
|
for (let attempt = 1;; attempt++) {
|
|
1174
1181
|
const result = await runHdiutil(args);
|
|
1175
1182
|
if (result.success)
|
|
@@ -1179,17 +1186,25 @@ async function createDiskImage(opts) {
|
|
|
1179
1186
|
rmSync2(writable, { force: true });
|
|
1180
1187
|
await new Promise((resolve) => setTimeout(resolve, attempt * 2000));
|
|
1181
1188
|
}
|
|
1182
|
-
|
|
1189
|
+
phase = "attach";
|
|
1190
|
+
attachment = (await run("hdiutil", ["attach", writable, "-nobrowse", "-mountpoint", mount])).stdout;
|
|
1183
1191
|
mounted = true;
|
|
1192
|
+
phase = "layout";
|
|
1184
1193
|
writeInstallerLayout(mount, opts.volumeName, bundleName);
|
|
1194
|
+
phase = "detach";
|
|
1185
1195
|
const detached = await detachDiskImage(mount);
|
|
1186
|
-
if (!detached.success)
|
|
1196
|
+
if (!detached.success) {
|
|
1197
|
+
detachAttempts = detached.attempts;
|
|
1187
1198
|
throw new Error(detached.error);
|
|
1199
|
+
}
|
|
1188
1200
|
mounted = false;
|
|
1201
|
+
phase = "convert";
|
|
1189
1202
|
await run("hdiutil", ["convert", writable, "-format", "ULMO", "-ov", "-o", opts.outputPath]);
|
|
1190
1203
|
return { success: true, outputPath: opts.outputPath };
|
|
1191
1204
|
} catch (error) {
|
|
1192
|
-
|
|
1205
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1206
|
+
return { success: false, error: `${message}
|
|
1207
|
+
Disk image lifecycle: ${JSON.stringify({ phase, image: writable, mount, attachment, detachAttempts })}` };
|
|
1193
1208
|
} finally {
|
|
1194
1209
|
if (mounted)
|
|
1195
1210
|
await runTool("hdiutil", ["detach", mount, "-force"]);
|
|
@@ -819,6 +819,7 @@ final class CraftChrome: ObservableObject {
|
|
|
819
819
|
var image: String? = nil
|
|
820
820
|
/// A compact floating icon, instead of the system full-width accessory shelf.
|
|
821
821
|
var iconOnly = false
|
|
822
|
+
var companions: [Tab] = []
|
|
822
823
|
}
|
|
823
824
|
|
|
824
825
|
@Published var tabs: [Tab] = []
|
|
@@ -828,8 +829,12 @@ final class CraftChrome: ObservableObject {
|
|
|
828
829
|
/// The visible destination may be the accessory, independently of its owning stack.
|
|
829
830
|
@Published var visiblePath: String?
|
|
830
831
|
var accessorySelected: Bool {
|
|
831
|
-
guard let
|
|
832
|
-
return
|
|
832
|
+
guard let accessory = tabAccessory else { return false }
|
|
833
|
+
return ([accessory] + accessory.companions).contains { accessorySelected($0) }
|
|
834
|
+
}
|
|
835
|
+
func accessorySelected(_ accessory: Tab) -> Bool {
|
|
836
|
+
guard let path = visiblePath else { return false }
|
|
837
|
+
return path == accessory.id || path.hasPrefix(accessory.id + "/")
|
|
833
838
|
}
|
|
834
839
|
var highlightedTab: String? { accessorySelected ? nil : selected }
|
|
835
840
|
let tabSwipeProgress = CraftTabSwipeProgress()
|
|
@@ -954,7 +959,14 @@ final class CraftChrome: ObservableObject {
|
|
|
954
959
|
next = Tab(id: path, title: title, symbol: item["symbol"] as? String ?? "message",
|
|
955
960
|
badge: (item["badge"] as? String).flatMap { $0.isEmpty ? nil : String($0.prefix(8)) },
|
|
956
961
|
accessibilityLabel: item["accessibilityLabel"] as? String,
|
|
957
|
-
iconOnly: item["iconOnly"] as? Bool ?? false
|
|
962
|
+
iconOnly: item["iconOnly"] as? Bool ?? false,
|
|
963
|
+
companions: ((item["companions"] as? [[String: Any]]) ?? []).prefix(2).compactMap { action in
|
|
964
|
+
guard let companionPath = action["path"] as? String,
|
|
965
|
+
companionPath.hasPrefix("/"), !companionPath.hasPrefix("//"), companionPath != path,
|
|
966
|
+
let companionTitle = action["title"] as? String else { return nil }
|
|
967
|
+
return Tab(id: companionPath, title: companionTitle, symbol: action["symbol"] as? String ?? "circle",
|
|
968
|
+
accessibilityLabel: action["accessibilityLabel"] as? String, iconOnly: true)
|
|
969
|
+
})
|
|
958
970
|
} else {
|
|
959
971
|
next = nil
|
|
960
972
|
}
|
|
@@ -1265,43 +1277,45 @@ private struct CraftTabBarContent: View {
|
|
|
1265
1277
|
if !chrome.tabs.isEmpty {
|
|
1266
1278
|
HStack(spacing: 8) {
|
|
1267
1279
|
bar
|
|
1268
|
-
if let
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1280
|
+
if let primary = chrome.tabAccessory {
|
|
1281
|
+
ForEach(primary.companions + [primary]) { accessory in
|
|
1282
|
+
Button { CraftHybrid.shared.open(accessory.id) } label: {
|
|
1283
|
+
surface {
|
|
1284
|
+
Image(systemName: accessory.symbol)
|
|
1285
|
+
.font(.system(size: iconSize))
|
|
1286
|
+
.foregroundStyle(chrome.accessorySelected(accessory) ? selectedColor : Color.primary)
|
|
1287
|
+
.frame(width: 62, height: 62)
|
|
1288
|
+
.background {
|
|
1289
|
+
if chrome.accessorySelected(accessory) {
|
|
1290
|
+
Circle().fill(selectedColor.opacity(0.14)).padding(4)
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
.contentShape(Rectangle())
|
|
1295
|
+
.overlay(alignment: .topTrailing) {
|
|
1296
|
+
if let badge = accessory.badge {
|
|
1297
|
+
Text(badge)
|
|
1298
|
+
.font(.system(size: badgeSize, weight: .bold))
|
|
1299
|
+
.foregroundStyle(.white)
|
|
1300
|
+
.padding(.horizontal, 4)
|
|
1301
|
+
.frame(minWidth: 18, minHeight: 18)
|
|
1302
|
+
.background(Capsule().fill(Color.red))
|
|
1278
1303
|
}
|
|
1279
1304
|
}
|
|
1280
1305
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
.buttonStyle(.plain)
|
|
1294
|
-
.accessibilityShowsLargeContentViewer {
|
|
1295
|
-
Image(systemName: accessory.symbol)
|
|
1296
|
-
Text(accessory.accessibilityLabel ?? accessory.title)
|
|
1306
|
+
.buttonStyle(.plain)
|
|
1307
|
+
.accessibilityShowsLargeContentViewer {
|
|
1308
|
+
Image(systemName: accessory.symbol)
|
|
1309
|
+
Text(accessory.accessibilityLabel ?? accessory.title)
|
|
1310
|
+
}
|
|
1311
|
+
// Keep accessibility intent for the viewer; displayed
|
|
1312
|
+
// sizes come from the bounded metrics owned above.
|
|
1313
|
+
.environment(\.dynamicTypeSize, viewerTextSize)
|
|
1314
|
+
.accessibilityLabel(accessory.accessibilityLabel ?? accessory.title)
|
|
1315
|
+
.accessibilityValue(accessory.badge ?? "")
|
|
1316
|
+
.accessibilityIdentifier("craft-tab-accessory")
|
|
1317
|
+
.accessibilityAddTraits(chrome.accessorySelected(accessory) ? .isSelected : [])
|
|
1297
1318
|
}
|
|
1298
|
-
// Keep accessibility intent for the viewer; displayed
|
|
1299
|
-
// sizes come from the bounded metrics owned above.
|
|
1300
|
-
.environment(\.dynamicTypeSize, viewerTextSize)
|
|
1301
|
-
.accessibilityLabel(accessory.accessibilityLabel ?? accessory.title)
|
|
1302
|
-
.accessibilityValue(accessory.badge ?? "")
|
|
1303
|
-
.accessibilityIdentifier("craft-tab-accessory")
|
|
1304
|
-
.accessibilityAddTraits(chrome.accessorySelected ? .isSelected : [])
|
|
1305
1319
|
}
|
|
1306
1320
|
}
|
|
1307
1321
|
.padding(.horizontal, 16)
|
|
@@ -1192,6 +1192,9 @@ final class CraftHybridController: UITabBarController, UITabBarControllerDelegat
|
|
|
1192
1192
|
private var synchronizingTabs = false
|
|
1193
1193
|
private var accessoryModel: CraftChrome.Tab?
|
|
1194
1194
|
private let accessoryButton = UIButton(type: .system)
|
|
1195
|
+
private var companionButtons: [UIButton] = []
|
|
1196
|
+
private var companionModels: [CraftChrome.Tab] = []
|
|
1197
|
+
private var companionGlasses: [UIVisualEffectView] = []
|
|
1195
1198
|
private let accessoryBadge = UILabel()
|
|
1196
1199
|
private let accessoryGlass = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
|
1197
1200
|
private static let tabPictures = NSCache<NSURL, UIImage>()
|
|
@@ -1410,6 +1413,40 @@ final class CraftHybridController: UITabBarController, UITabBarControllerDelegat
|
|
|
1410
1413
|
private func updateSystemAccessory(hidden: Bool) {
|
|
1411
1414
|
let chrome = CraftChrome.shared
|
|
1412
1415
|
let model = hidden ? nil : chrome.tabAccessory
|
|
1416
|
+
// Keep controls when only the selected destination or unread badge changes.
|
|
1417
|
+
let companions = model?.iconOnly == true ? model?.companions ?? [] : []
|
|
1418
|
+
if companionModels != companions {
|
|
1419
|
+
companionModels = companions
|
|
1420
|
+
for button in companionButtons { button.removeFromSuperview() }
|
|
1421
|
+
for glass in companionGlasses { glass.removeFromSuperview() }
|
|
1422
|
+
companionButtons.removeAll()
|
|
1423
|
+
companionGlasses.removeAll()
|
|
1424
|
+
for action in companions {
|
|
1425
|
+
let button = UIButton(type: .system)
|
|
1426
|
+
var configuration = UIButton.Configuration.plain()
|
|
1427
|
+
configuration.image = UIImage(systemName: action.symbol)
|
|
1428
|
+
button.configuration = configuration
|
|
1429
|
+
button.accessibilityIdentifier = "craft-tab-companion:" + action.id
|
|
1430
|
+
button.accessibilityLabel = action.accessibilityLabel ?? action.title
|
|
1431
|
+
button.accessibilityValue = action.badge
|
|
1432
|
+
button.accessibilityTraits = chrome.accessorySelected(action) ? [.button, .selected] : [.button]
|
|
1433
|
+
button.addAction(UIAction { [weak self] _ in self?.hybrid.open(action.id) }, for: .touchUpInside)
|
|
1434
|
+
let glass = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
|
1435
|
+
#if compiler(>=6.2)
|
|
1436
|
+
if #available(iOS 26.0, *) { glass.effect = UIGlassEffect() }
|
|
1437
|
+
#endif
|
|
1438
|
+
glass.layer.cornerRadius = 24
|
|
1439
|
+
glass.clipsToBounds = true
|
|
1440
|
+
glass.isUserInteractionEnabled = false
|
|
1441
|
+
view.addSubview(glass)
|
|
1442
|
+
view.addSubview(button)
|
|
1443
|
+
companionGlasses.append(glass)
|
|
1444
|
+
companionButtons.append(button)
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
for (index, action) in companionModels.enumerated() {
|
|
1448
|
+
companionButtons[index].accessibilityTraits = chrome.accessorySelected(action) ? [.button, .selected] : [.button]
|
|
1449
|
+
}
|
|
1413
1450
|
if let model {
|
|
1414
1451
|
var configuration = UIButton.Configuration.plain()
|
|
1415
1452
|
configuration.image = UIImage(systemName: model.symbol)
|
|
@@ -1419,7 +1456,7 @@ final class CraftHybridController: UITabBarController, UITabBarControllerDelegat
|
|
|
1419
1456
|
accessoryButton.configuration = configuration
|
|
1420
1457
|
accessoryButton.accessibilityLabel = model.accessibilityLabel ?? model.displayTitle
|
|
1421
1458
|
accessoryButton.accessibilityValue = model.badge
|
|
1422
|
-
accessoryButton.accessibilityTraits = chrome.accessorySelected ? [.button, .selected] : [.button]
|
|
1459
|
+
accessoryButton.accessibilityTraits = chrome.accessorySelected(model) ? [.button, .selected] : [.button]
|
|
1423
1460
|
}
|
|
1424
1461
|
#if compiler(>=6.2)
|
|
1425
1462
|
if #available(iOS 26.0, *), model?.iconOnly != true {
|
|
@@ -1493,6 +1530,11 @@ final class CraftHybridController: UITabBarController, UITabBarControllerDelegat
|
|
|
1493
1530
|
let x = iconOnly ? view.bounds.maxX - view.safeAreaInsets.right - width - 20 : 16
|
|
1494
1531
|
accessoryButton.frame = CGRect(x: x, y: tabBar.frame.minY - height - (iconOnly ? 8 : 0), width: width, height: height)
|
|
1495
1532
|
accessoryGlass.frame = accessoryButton.frame
|
|
1533
|
+
for (index, button) in companionButtons.enumerated() {
|
|
1534
|
+
// Array order reads left to right, ending at the primary action.
|
|
1535
|
+
button.frame = accessoryButton.frame.offsetBy(dx: -CGFloat(companionButtons.count - index) * (width + 12), dy: 0)
|
|
1536
|
+
companionGlasses[index].frame = button.frame
|
|
1537
|
+
}
|
|
1496
1538
|
let badgeWidth = max(18, accessoryBadge.intrinsicContentSize.width + 8)
|
|
1497
1539
|
accessoryBadge.frame = CGRect(x: accessoryButton.frame.maxX - badgeWidth + 4, y: accessoryButton.frame.minY - 3, width: badgeWidth, height: 18)
|
|
1498
1540
|
// A compact action floats over the page, not across its whole width.
|
|
@@ -7856,6 +7856,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7856
7856
|
/// the home indicator too, where its content never reaches: a black
|
|
7857
7857
|
/// full-screen timer is black edge to edge. nil is the app's.
|
|
7858
7858
|
private var screenBackgroundColor: UIColor?
|
|
7859
|
+
/// Title contrast belongs to this screen, independently of the bar material.
|
|
7860
|
+
private var navigationTitleColor: UIColor?
|
|
7859
7861
|
|
|
7860
7862
|
override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
7861
7863
|
statusBarStyleOverride ?? .default
|
|
@@ -7926,6 +7928,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7926
7928
|
if let solid = options["navigationBarSolid"] as? Bool {
|
|
7927
7929
|
navigationBarSolid = solid
|
|
7928
7930
|
}
|
|
7931
|
+
if options.keys.contains("titleColor") {
|
|
7932
|
+
navigationTitleColor = color(options["titleColor"])
|
|
7933
|
+
}
|
|
7929
7934
|
if let hidden = options["hidden"] as? Bool {
|
|
7930
7935
|
navigationBarHidden = hidden
|
|
7931
7936
|
if navigationController?.topViewController === self {
|
|
@@ -7941,7 +7946,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7941
7946
|
let buttons = (options["rightButtons"] as? [[String: Any]]) ?? []
|
|
7942
7947
|
navigationItem.rightBarButtonItems = buttons.compactMap(navigationButton).reversed()
|
|
7943
7948
|
}
|
|
7944
|
-
if options.keys.contains("backgroundColor") || options.keys.contains("navigationBarSolid") {
|
|
7949
|
+
if options.keys.contains("backgroundColor") || options.keys.contains("navigationBarSolid") || options.keys.contains("titleColor") {
|
|
7945
7950
|
applyNavigationBarAppearance()
|
|
7946
7951
|
trackContentScrollView()
|
|
7947
7952
|
}
|
|
@@ -7952,25 +7957,44 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7952
7957
|
}
|
|
7953
7958
|
}
|
|
7954
7959
|
|
|
7955
|
-
///
|
|
7956
|
-
///
|
|
7960
|
+
/// Per-item appearances preserve each UIKit bar state and never recolour a
|
|
7961
|
+
/// sibling controller's title or opt a material bar into an opaque fill.
|
|
7957
7962
|
private func applyNavigationBarAppearance() {
|
|
7958
|
-
guard navigationBarSolid else {
|
|
7963
|
+
guard navigationBarSolid || navigationTitleColor != nil else {
|
|
7959
7964
|
navigationItem.standardAppearance = nil
|
|
7960
7965
|
navigationItem.scrollEdgeAppearance = nil
|
|
7961
7966
|
navigationItem.compactAppearance = nil
|
|
7962
7967
|
navigationItem.compactScrollEdgeAppearance = nil
|
|
7963
7968
|
return
|
|
7964
7969
|
}
|
|
7965
|
-
let
|
|
7966
|
-
|
|
7967
|
-
appearance
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7970
|
+
let bar = navigationController?.navigationBar
|
|
7971
|
+
let standard = bar?.standardAppearance ?? UINavigationBarAppearance()
|
|
7972
|
+
// UIKit's unspecified scroll-edge appearance uses a transparent
|
|
7973
|
+
// background. Copy its standard text/button attributes before applying
|
|
7974
|
+
// that background so adding title contrast preserves the same states.
|
|
7975
|
+
let scrollEdge = bar?.scrollEdgeAppearance ?? {
|
|
7976
|
+
let appearance = UINavigationBarAppearance(barAppearance: standard)
|
|
7977
|
+
appearance.configureWithTransparentBackground()
|
|
7978
|
+
return appearance
|
|
7979
|
+
}()
|
|
7980
|
+
func styled(_ base: UINavigationBarAppearance) -> UINavigationBarAppearance {
|
|
7981
|
+
let appearance = UINavigationBarAppearance(barAppearance: base)
|
|
7982
|
+
if navigationBarSolid {
|
|
7983
|
+
appearance.configureWithOpaqueBackground()
|
|
7984
|
+
appearance.backgroundColor = screenBackgroundColor ?? config.resolvedBackgroundColor
|
|
7985
|
+
appearance.shadowColor = .clear
|
|
7986
|
+
}
|
|
7987
|
+
CraftChromeFonts.apply(to: appearance)
|
|
7988
|
+
if let navigationTitleColor {
|
|
7989
|
+
appearance.titleTextAttributes[.foregroundColor] = navigationTitleColor
|
|
7990
|
+
appearance.largeTitleTextAttributes[.foregroundColor] = navigationTitleColor
|
|
7991
|
+
}
|
|
7992
|
+
return appearance
|
|
7993
|
+
}
|
|
7994
|
+
navigationItem.standardAppearance = styled(standard)
|
|
7995
|
+
navigationItem.scrollEdgeAppearance = styled(scrollEdge)
|
|
7996
|
+
navigationItem.compactAppearance = styled(bar?.compactAppearance ?? standard)
|
|
7997
|
+
navigationItem.compactScrollEdgeAppearance = styled(bar?.compactScrollEdgeAppearance ?? scrollEdge)
|
|
7974
7998
|
}
|
|
7975
7999
|
|
|
7976
8000
|
private func applyStatusBar(_ props: [String: Any]) {
|
|
@@ -11444,6 +11468,17 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11444
11468
|
// UISwitch also owns its native identity and enabled/on state.
|
|
11445
11469
|
// Assigning .button changes its legacy AX type into a button while
|
|
11446
11470
|
// modern XCTest still identifies its UIKit class as a switch.
|
|
11471
|
+
// The solid variant publishes "1"/"0" as a string from
|
|
11472
|
+
// refreshAccessibility(). A UISwitch left to UIKit does not: the value
|
|
11473
|
+
// it hands XCTest arrives as a number, so every `value as? String`
|
|
11474
|
+
// read finds nothing. The driver's flip check is one of those reads,
|
|
11475
|
+
// which is why a switch that did flip was reported as never flipping,
|
|
11476
|
+
// and only sometimes - it depends which representation the accessibility
|
|
11477
|
+
// snapshot was built from. Publishing the string makes both variants
|
|
11478
|
+
// answer the same way. An authored accessibilityValue still wins.
|
|
11479
|
+
if let toggle = view as? UISwitch, !authoredAccessibilityValues.contains(id) {
|
|
11480
|
+
view.accessibilityValue = toggle.isOn ? "1" : "0"
|
|
11481
|
+
}
|
|
11447
11482
|
if view is UITextView || view is UISwitch { return }
|
|
11448
11483
|
if let toggle = view as? CraftNativeSolidSwitch { toggle.refreshAccessibility(); return }
|
|
11449
11484
|
view.accessibilityTraits = traits
|
|
@@ -30,14 +30,22 @@ enum CraftPresenter {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/// Present over whatever is on screen. A controller that shows as a
|
|
33
|
-
/// popover
|
|
34
|
-
/// `view`, or sits in the middle of it with no
|
|
35
|
-
/// exception for a popover with neither. False
|
|
36
|
-
/// so the caller answers rather than waiting.
|
|
33
|
+
/// popover (an action sheet, a share sheet, on a regular-width screen)
|
|
34
|
+
/// points at `anchor` in `view`, or sits in the middle of it with no
|
|
35
|
+
/// arrow; iPadOS raises an exception for a popover with neither. False
|
|
36
|
+
/// when nothing can present, so the caller answers rather than waiting.
|
|
37
|
+
///
|
|
38
|
+
/// Only where the presentation really is a popover. A `.actionSheet`
|
|
39
|
+
/// always carries a `popoverPresentationController`, phone included, and
|
|
40
|
+
/// from iOS 26 configuring it is enough to make the phone present the
|
|
41
|
+
/// sheet as a centred popover -- which silently drops its cancel action,
|
|
42
|
+
/// as UIKit never draws one in a popover. A compact screen is left alone
|
|
43
|
+
/// and gets the standard sheet, with its Cancel.
|
|
37
44
|
@discardableResult
|
|
38
45
|
static func present(_ controller: UIViewController, from view: UIView?, anchor: CGRect? = nil) -> Bool {
|
|
39
46
|
guard let top = topViewController() else { return false }
|
|
40
|
-
|
|
47
|
+
let asPopover = (view ?? top.view)?.traitCollection.horizontalSizeClass == .regular
|
|
48
|
+
if asPopover, let popover = controller.popoverPresentationController {
|
|
41
49
|
let source = view ?? top.view!
|
|
42
50
|
popover.sourceView = source
|
|
43
51
|
if let anchor {
|
package/dist/package.d.ts
CHANGED
|
@@ -273,6 +273,10 @@ export type PackagingToolResult = {
|
|
|
273
273
|
success: false;
|
|
274
274
|
error: string;
|
|
275
275
|
};
|
|
276
|
+
interface DiskImageDetachAttempt {
|
|
277
|
+
args: string[];
|
|
278
|
+
error: string;
|
|
279
|
+
}
|
|
276
280
|
/**
|
|
277
281
|
* Eject a mounted image, retrying the contention hdiutil reports as
|
|
278
282
|
* "couldn't eject ... - Resource busy".
|
|
@@ -292,6 +296,7 @@ export declare function detachDiskImage(mount: string, options?: {
|
|
|
292
296
|
} | {
|
|
293
297
|
success: false;
|
|
294
298
|
error: string;
|
|
299
|
+
attempts: DiskImageDetachAttempt[];
|
|
295
300
|
}>;
|
|
296
301
|
/**
|
|
297
302
|
* Helper: Create DMG from app bundle
|