ReviewOS

also looking at this

craft-native/craft

feat: finish native customization and platform seams

#401
Merged glennmichael123 wants to merge codex/issues-388-400-344 into main
21 files +707 -17
packages/android/templates/CraftNativeShell.kt.templatemodified+32-1
Changes to packages/android/templates/CraftNativeShell.kt.template
@@ -86,6 +86,11 @@ internal data class CraftNativeTab(
8686 val selectedSymbol: String? = null, val selectedIconTint: String? = null, val selectedIconPulse: Boolean? = null,
8787)
8888
89internal data class CraftNativeTabBarAppearance(
90 val selectionColor: String? = null,
91 val selectionCornerRadius: Float? = null,
92)
93
8994internal data class CraftTabIconAppearance(val symbol: String, val tint: Int?, val pulse: Boolean)
9095
9196/** The app's paths (`nativeScreens`), its tabs and its site. */
@@ -122,6 +127,16 @@ internal class CraftNativeRoutes(config: JSONObject) {
122127 }
123128 }
124129
130 val tabBarAppearance: CraftNativeTabBarAppearance = config.optJSONObject("tabBar")
131 ?.optJSONObject("selection")
132 ?.let { selection ->
133 CraftNativeTabBarAppearance(
134 selection.optString("color").takeIf { it.isNotBlank() },
135 (selection.opt("cornerRadius") as? Number)?.toFloat()?.takeIf { it.isFinite() && it >= 0 },
136 )
137 }
138 ?: CraftNativeTabBarAppearance()
139
125140 /** The app's site (`devServerURL`): its origin answers paths no screen draws. */
126141 val site: Uri? = config.optString("devServerURL").takeIf { it.isNotBlank() }?.let(Uri::parse)
127142 val siteOrigin: String? = site?.let { uri ->
@@ -755,7 +770,11 @@ internal object CraftNativePrelude {
755770 * `craft.navigation.setOptions` (title, largeTitle, hidden, rightButtons,
756771 * tabAccessory, tabBarHidden); the tabs come from the app's `tabs`.
757772 */
758internal class CraftNativeChrome(context: Context, private val tabs: List<CraftNativeTab>) : LinearLayout(context) {
773internal class CraftNativeChrome(
774 context: Context,
775 private val tabs: List<CraftNativeTab>,
776 private val tabBarAppearance: CraftNativeTabBarAppearance = CraftNativeTabBarAppearance(),
777) : LinearLayout(context) {
759778 val content = FrameLayout(context)
760779 private val bar = LinearLayout(context)
761780 private val backButton = ImageView(context)
@@ -1099,6 +1118,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
10991118 accessorySpec?.let { accessoryIcon.setImageDrawable(CraftNativeIcons.drawable(context, null, it.optString("symbol"), text)) }
11001119 tabViews.forEachIndexed { index, (icon, label) ->
11011120 tabBar.getChildAt(index).isSelected = index == selectedTab
1121 tabBar.getChildAt(index).background = tabCellBackground(index, dark)
11021122 val color = if (index == selectedTab) accent else muted
11031123 val id = tabs[index].id
11041124 val number = tabIconText[id]
@@ -1188,6 +1208,17 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
11881208
11891209 private fun accentColor(dark: Boolean): Int = if (dark) Color.parseColor("#0A84FF") else Color.parseColor("#007AFF")
11901210
1211 private fun tabCellBackground(index: Int, dark: Boolean): android.graphics.drawable.Drawable? {
1212 if (index != selectedTab || (tabBarAppearance.selectionColor == null && tabBarAppearance.selectionCornerRadius == null)) return rippleBackground()
1213 val color = tabBarAppearance.selectionColor?.let { runCatching { Color.parseColor(it) }.getOrNull() }
1214 ?: if (dark) Color.argb(42, 255, 255, 255) else Color.argb(23, 0, 0, 0)
1215 val radius = tabBarAppearance.selectionCornerRadius?.let { (it * density).toInt().toFloat() } ?: dp(32).toFloat()
1216 return GradientDrawable().apply {
1217 setColor(color)
1218 cornerRadius = radius
1219 }
1220 }
1221
11911222 private fun rippleBackground(): android.graphics.drawable.Drawable? {
11921223 val value = TypedValue()
11931224 return if (context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, value, true)) {