peachsim 0.1.660 → 0.1.661

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +10 -15
  2. package/cli/drivers/linux-browser-libs.lock.json +1088 -0
  3. package/cli/drivers/linux-browser-libs.ts +182 -0
  4. package/cli/drivers/playwright-provisioning.ts +20 -98
  5. package/cli/drivers/playwright-sim-host.ts +9 -0
  6. package/cli/drivers/playwright.ts +2 -1
  7. package/dist-lib/agent-daemon-client.cjs +1 -1
  8. package/dist-lib/agent-events.cjs +1 -1
  9. package/dist-lib/agent-identity.cjs +1 -1
  10. package/dist-lib/agent-sessions.cjs +1 -1
  11. package/dist-lib/attached-projects.cjs +1 -1
  12. package/dist-lib/auth/shared-session.cjs +1 -1
  13. package/dist-lib/backend-origin.cjs +18 -1
  14. package/dist-lib/beta.cjs +1 -1
  15. package/dist-lib/beta.mjs +1 -1
  16. package/dist-lib/bridge-constants.cjs +1 -1
  17. package/dist-lib/bridge-contract-input.cjs +1 -1
  18. package/dist-lib/bridge-contract-input.mjs +1 -1
  19. package/dist-lib/bridge-contract.cjs +1 -1
  20. package/dist-lib/bridge-contract.mjs +1 -1
  21. package/dist-lib/capture-contract.cjs +1 -1
  22. package/dist-lib/capture-contract.mjs +1 -1
  23. package/dist-lib/cli-constants.cjs +1 -1
  24. package/dist-lib/cloud-contract.cjs +1 -1
  25. package/dist-lib/cloud-contract.mjs +1 -1
  26. package/dist-lib/cloud-test-args.cjs +1 -1
  27. package/dist-lib/cloud-test-args.mjs +1 -1
  28. package/dist-lib/cloud.cjs +1 -1
  29. package/dist-lib/cloud.mjs +1 -1
  30. package/dist-lib/config.cjs +1 -1
  31. package/dist-lib/detox/index.cjs +1 -1
  32. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  33. package/dist-lib/home-paths.cjs +1 -1
  34. package/dist-lib/host/bridge-host.cjs +45 -41
  35. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  36. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  37. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  38. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  39. package/dist-lib/host/websocket-proxy.cjs +1 -1
  40. package/dist-lib/index.cjs +1 -1
  41. package/dist-lib/jump-to-source-babel.cjs +1 -1
  42. package/dist-lib/jump-to-source-native.cjs +1 -1
  43. package/dist-lib/kotlin-host.cjs +4 -3
  44. package/dist-lib/menu.cjs +1 -1
  45. package/dist-lib/menu.mjs +1 -1
  46. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  47. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  48. package/dist-lib/metro-production-bundle.cjs +1 -1
  49. package/dist-lib/metro-production-bundle.mjs +1 -1
  50. package/dist-lib/metro.cjs +1 -1
  51. package/dist-lib/native-app-identity.cjs +1 -1
  52. package/dist-lib/native-app-identity.mjs +1 -1
  53. package/dist-lib/plist.cjs +1 -1
  54. package/dist-lib/plist.mjs +1 -1
  55. package/dist-lib/profiles.cjs +1 -1
  56. package/dist-lib/public-brand.cjs +1 -1
  57. package/dist-lib/react-native-host-modules.cjs +1 -1
  58. package/dist-lib/react-native-host-modules.mjs +1 -1
  59. package/dist-lib/render-mode.cjs +1 -1
  60. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  61. package/dist-lib/sdk.cjs +1 -1
  62. package/dist-lib/sdk.mjs +1 -1
  63. package/dist-lib/skills.cjs +1332 -177
  64. package/dist-lib/swift-compile.cjs +1 -1
  65. package/dist-lib/swift-compile.mjs +1 -1
  66. package/dist-lib/swift-package.cjs +1 -1
  67. package/dist-lib/swift-package.mjs +1 -1
  68. package/dist-lib/swift-submit.cjs +1 -1
  69. package/dist-lib/swift-submit.mjs +1 -1
  70. package/dist-lib/swift.cjs +1 -1
  71. package/dist-lib/vite-swift.cjs +1 -1
  72. package/dist-lib/vite.cjs +1 -1
  73. package/kotlin/AppSources/android/view/Window.kt +15 -9
  74. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +29 -16
  75. package/package.json +2 -1
  76. package/src/android-theme.ts +14 -0
  77. package/src/backend-origin.ts +25 -0
  78. package/src/host/bridge-host.ts +24 -37
  79. package/src/kotlin-app.ts +22 -4
  80. package/src/kotlin-host.ts +3 -2
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.660 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.660 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-compile.ts
4
4
  import { execFile as execFile2 } from "node:child_process";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.660 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.660 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-package.ts
4
4
  function isSwiftManifestFile(file) {
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.660 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.660 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/swift-submit.ts
4
4
  var SWIFT_COMPILE_PATH = "/api/swift-compile";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.660 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.660 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.660 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.661 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -13,6 +13,7 @@ import org.json.JSONObject
13
13
  internal class WindowTheme(
14
14
  val edgeToEdgeEnforced: Boolean,
15
15
  val statusBarColor: Int,
16
+ val navigationBarColor: Int,
16
17
  val statusBarContrastEnforced: Boolean,
17
18
  val navigationBarContrastEnforced: Boolean,
18
19
  val lightStatusBar: Boolean,
@@ -62,15 +63,17 @@ class Window internal constructor(
62
63
  fun getStatusBarColor(): Int = statusBarColor
63
64
  fun setStatusBarColor(color: Int) { statusBarColor = color }
64
65
 
65
- // the theme's navigation bar color resolves through device configuration
66
- // peach kotlin does not model, so it reads only once the app sets it; an
67
- // enforced edge to edge window reports transparent
68
- private var navigationBar: Int? = null
66
+ // the theme's navigation bar color as PhoneWindow resolved it; an enforced
67
+ // edge to edge window reports and draws transparent
68
+ private var navigationBar = theme.navigationBarColor
69
69
 
70
70
  var navigationBarColor: Int
71
- get() = if (edgeToEdgeEnforced) Color.TRANSPARENT
72
- else checkNotNull(navigationBar) { "the theme's navigation bar color is not modeled; read it after setting it" }
73
- set(color) { navigationBar = color }
71
+ get() = if (edgeToEdgeEnforced) Color.TRANSPARENT else navigationBar
72
+ set(color) {
73
+ if (color == navigationBar) return
74
+ navigationBar = color
75
+ host.windowChanged()
76
+ }
74
77
 
75
78
  fun getNavigationBarColor(): Int = navigationBarColor
76
79
  fun setNavigationBarColor(color: Int) { navigationBarColor = color }
@@ -113,11 +116,14 @@ class Window internal constructor(
113
116
  return SCRIM_LIGHT
114
117
  }
115
118
 
116
- // what the decor draws for this window: its inset policy and its status
117
- // bar color as unsigned ARGB
119
+ // what the decor draws for this window: its inset policy and its bar
120
+ // colors as unsigned ARGB. gesture navigation draws no contrast scrim
121
+ // behind a transparent navigation bar, as the oracle's edge to edge
122
+ // captures show
118
123
  internal fun describe(): JSONObject = JSONObject()
119
124
  .put("decorFitsSystemWindows", fits)
120
125
  .put("statusBarColor", drawnStatusBarColor().toLong() and 0xffffffffL)
126
+ .put("navigationBarColor", navigationBarColor.toLong() and 0xffffffffL)
121
127
 
122
128
  // Activity.setContentView(View) fills the content parent
123
129
  internal fun setContentView(view: View, params: ViewGroup.LayoutParams) {
@@ -53,19 +53,32 @@ internal class PeachAppSpec(
53
53
  val handlesUiMode: Boolean,
54
54
  )
55
55
 
56
- // the system bars the device delivers to the window's content, in pixels: all
57
- // of them on an edge-to-edge window, none once the decor fits system windows.
58
- private class SystemBarInsets(
59
- private val window: Window,
60
- private val status: Int,
61
- private val navigation: Int,
62
- ) : PlatformWindowInsets {
56
+ // the insets the device delivers to the window's content, in pixels: all of
57
+ // them on an edge-to-edge window, none once the decor fits system windows.
58
+ // insets lists the status bars, navigation bars, display cutout, system
59
+ // gestures, mandatory system gestures and tappable element, each left, top,
60
+ // right and bottom.
61
+ private class DeviceInsets(private val window: Window, insets: String) : PlatformWindowInsets {
62
+ private val pixels = insets.split(',').map { it.toInt() }.also { require(it.size == 24) }
63
+
63
64
  // read when compose lays out, so a later change to the window applies
64
- private fun delivered(pixels: Int) = if (window.fitsSystemWindows) 0 else pixels
65
- override val statusBars = PlatformInsets(getTop = { delivered(status) })
66
- override val navigationBars = PlatformInsets(getBottom = { delivered(navigation) })
67
- override val systemBars =
68
- PlatformInsets(getTop = { delivered(status) }, getBottom = { delivered(navigation) })
65
+ private fun type(index: Int): PlatformInsets {
66
+ fun side(offset: Int) = { if (window.fitsSystemWindows) 0 else pixels[index * 4 + offset] }
67
+ return PlatformInsets(side(0), side(1), side(2), side(3))
68
+ }
69
+
70
+ override val statusBars = type(0)
71
+ override val navigationBars = type(1)
72
+ override val displayCutout = type(2)
73
+ override val systemGestures = type(3)
74
+ override val mandatorySystemGestures = type(4)
75
+ override val tappableElement = type(5)
76
+ override val systemBars = PlatformInsets(
77
+ { maxOf(statusBars.left, navigationBars.left) },
78
+ { maxOf(statusBars.top, navigationBars.top) },
79
+ { maxOf(statusBars.right, navigationBars.right) },
80
+ { maxOf(statusBars.bottom, navigationBars.bottom) },
81
+ )
69
82
  }
70
83
 
71
84
  private class ActivityOwners(activity: ComponentActivity) : PlatformArchitectureComponentsOwner {
@@ -85,7 +98,7 @@ private class AppProcess(
85
98
  val application: Application,
86
99
  val density: Float, val fontScale: Float, val locale: String, val gpu: Boolean, val stencilBits: Int,
87
100
  val fonts: FontFamily,
88
- val statusBar: Int, val navigationBar: Int,
101
+ val insets: String,
89
102
  val invalidate: (Boolean) -> Unit,
90
103
  ) {
91
104
  lateinit var activity: Activity
@@ -120,7 +133,7 @@ internal fun peachStartApp(
120
133
  spec: PeachAppSpec,
121
134
  density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,
122
135
  regular: String, medium: String, bold: String,
123
- statusBar: Int, navigationBar: Int, night: Boolean,
136
+ insets: String, night: Boolean,
124
137
  invalidate: (Boolean) -> Unit,
125
138
  ): String {
126
139
  try {
@@ -131,7 +144,7 @@ internal fun peachStartApp(
131
144
  application.onCreate()
132
145
  val app = AppProcess(
133
146
  spec, application, density, fontScale, locale, backend == "graphite", stencilBits,
134
- robotoFamily(regular, medium, bold), statusBar, navigationBar, invalidate,
147
+ robotoFamily(regular, medium, bold), insets, invalidate,
135
148
  )
136
149
  process = app
137
150
  val view = launchActivity(app, null)
@@ -201,7 +214,7 @@ private fun launchActivity(app: AppProcess, lastNonConfigurationInstance: Any?):
201
214
  app.density, app.fontScale, app.locale, app.gpu, app.stencilBits, app.fonts, app.invalidate,
202
215
  night = isNight(activity.resources.configuration),
203
216
  owners = componentActivity?.let(::ActivityOwners),
204
- insets = SystemBarInsets(window, app.statusBar, app.navigationBar),
217
+ insets = DeviceInsets(window, app.insets),
205
218
  clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
206
219
  )
207
220
  mount.setContent {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.660",
3
+ "version": "0.1.661",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -303,6 +303,7 @@
303
303
  "build:cli-binary": "bun scripts/build-cli-binary.ts",
304
304
  "smoke:cli-binary": "bun scripts/smoke-cli-binary.ts",
305
305
  "publish:cli-binary": "bun scripts/publish-cli-binaries.ts",
306
+ "publish:linux-browser-libs": "bun scripts/linux-browser-libs.ts publish",
306
307
  "pack:smoke": "bun scripts/pack-smoke.ts"
307
308
  },
308
309
  "dependencies": {
@@ -165,3 +165,17 @@ export function resolveThemeAttribute(
165
165
  // unset: the reader's own default applies, as PhoneWindow's getBoolean/getColor do
166
166
  return undefined
167
167
  }
168
+
169
+ // a framework color resource as context.getColor reads it, following
170
+ // references to other framework colors
171
+ export function frameworkColor(name: string, sources: ThemeSources): number {
172
+ let value = sources.android.colors[name]
173
+ while (value !== undefined && !value.startsWith('#')) {
174
+ const ref = /^@(?:android:)?color\/(.+)$/.exec(value)
175
+ if (!ref?.[1])
176
+ throw new Error(`@android:color/${name} has the unsupported value ${value}`)
177
+ value = sources.android.colors[ref[1]]
178
+ }
179
+ if (value === undefined) throw new Error(`@android:color/${name} is not defined`)
180
+ return color(value)
181
+ }
@@ -46,6 +46,31 @@ export function buildBrowserAssetProxyUrl(targetUrl: string): string {
46
46
  return `${proxyPath}?url=${encodeURIComponent(targetUrl)}`
47
47
  }
48
48
 
49
+ export function resolveBrowserAssetProxyUrl(
50
+ targetUrl: string,
51
+ currentOrigin: string,
52
+ ): string {
53
+ const proxyUrl = isBrowserAssetProxyUrl(targetUrl)
54
+ ? targetUrl
55
+ : buildBrowserAssetProxyUrl(targetUrl)
56
+ // loopback assets belong to the local bridge; external bytes to the backend.
57
+ if (proxyUrl.startsWith('/__bundle-proxy?')) return proxyUrl
58
+ return new URL(proxyUrl, resolveContrastBackendOrigin(currentOrigin)).toString()
59
+ }
60
+
61
+ export function isBrowserAssetProxyUrl(value: string): boolean {
62
+ try {
63
+ const url = new URL(value, DEFAULT_CONTRAST_BACKEND_ORIGIN)
64
+ return (
65
+ isContrastBackendOrigin(url.origin) &&
66
+ url.pathname === '/__fetch-proxy' &&
67
+ url.searchParams.has('url')
68
+ )
69
+ } catch {
70
+ return false
71
+ }
72
+ }
73
+
49
74
  export function isContrastBackendHost(hostname: string): boolean {
50
75
  const normalized = normalizeHostname(hostname)
51
76
  return (
@@ -592,24 +592,11 @@ export class SootSimBridgeHost {
592
592
  // subscriptions get cleaned up automatically on close.
593
593
  this.agentHost.registerSocket(ws)
594
594
 
595
- if (role === 'sim') {
596
- sim = {
597
- id: this.allocateSimId(),
598
- ws,
599
- origin,
600
- connectedAt: Date.now(),
601
- lastSeenAt: Date.now(),
602
- lastActiveAt: 0,
603
- recentActions: [],
604
- }
605
- this.sims.set(sim.id, sim)
606
- this.writeConnectedRuntimeSnapshot()
607
- if (this.shouldPromoteSim(sim)) {
608
- this.primarySimId = sim.id
609
- }
610
- this.broadcastSimAssignments()
611
- this.broadcastSimClientStates()
612
- } else {
595
+ // a browser socket becomes a sim when its page registers. the shell's
596
+ // agent bridge and other page helpers share this endpoint without ever
597
+ // registering, so giving them a sim id at connect listed ghost sims
598
+ // that took an id and could be neither driven nor closed.
599
+ if (role === 'cli') {
613
600
  const fallbackKey = `ws-${this.nextCliFallbackId++}`
614
601
  this.cliIdentityKeyBySocket.set(ws, fallbackKey)
615
602
  }
@@ -709,26 +696,30 @@ export class SootSimBridgeHost {
709
696
  // Origin-based role before this socket has registered a page. This reuses the existing
710
697
  // daemon transport and call envelope; it does not add a second routing
711
698
  // surface or let a registered sim switch roles mid-connection.
712
- if (role === 'sim' && msg.type === 'bridge:hello' && sim && !sim.url) {
713
- const provisionalId = sim.id
714
- this.sims.delete(provisionalId)
715
- if (this.primarySimId === provisionalId) {
716
- this.primarySimId = this.getOpenSim()?.id ?? null
717
- }
718
- sim = null
699
+ if (role === 'sim' && msg.type === 'bridge:hello' && !sim) {
719
700
  role = 'cli'
720
701
  this.cliIdentityKeyBySocket.set(ws, `ws-${this.nextCliFallbackId++}`)
721
- this.writeConnectedRuntimeSnapshot()
722
- this.broadcastSimAssignments()
723
- this.broadcastSimClientStates()
724
702
  }
725
703
 
726
704
  if (role === 'sim') {
727
705
  if (sim) {
728
706
  sim.lastSeenAt = Date.now()
729
707
  }
730
- if (msg.type === 'bridge:register' && sim) {
708
+ if (msg.type === 'bridge:register') {
731
709
  const registration = msg as BridgeSimRegistrationMessage
710
+ const firstRegistration = !sim
711
+ if (!sim) {
712
+ sim = {
713
+ id: this.allocateSimId(),
714
+ ws,
715
+ origin,
716
+ connectedAt: Date.now(),
717
+ lastSeenAt: Date.now(),
718
+ lastActiveAt: 0,
719
+ recentActions: [],
720
+ }
721
+ this.sims.set(sim.id, sim)
722
+ }
732
723
  const restored = this.tryRestoreSimId(sim, registration.simId)
733
724
  sim.url = registration.url
734
725
  sim.title = registration.title
@@ -743,16 +734,12 @@ export class SootSimBridgeHost {
743
734
  if (registration.meta && typeof registration.meta === 'object') {
744
735
  sim.meta = registration.meta as Record<string, unknown>
745
736
  }
746
- // re-evaluate primary now that the sim has a real page. promotion
747
- // is otherwise decided once at bare-connect, before `url` is
748
- // known — so a page-less sim that grabbed primary as the
749
- // last-resort fallback (or a stale zombie still holding it)
750
- // would never be superseded by this freshly-registered, actually
751
- // driveable sim (QA F19-2). only adopt a *different* sim so a
752
- // routine heartbeat re-register doesn't churn the assignment.
737
+ // elect primary once the sim has a page; only adopt a *different*
738
+ // sim so a routine heartbeat re-register doesn't churn the
739
+ // assignment.
753
740
  const reElected = this.primarySimId !== sim.id && this.shouldPromoteSim(sim)
754
741
  if (reElected) this.primarySimId = sim.id
755
- if (restored || reElected) {
742
+ if (firstRegistration || restored || reElected) {
756
743
  this.broadcastSimAssignments()
757
744
  this.broadcastSimClientStates()
758
745
  }
package/src/kotlin-app.ts CHANGED
@@ -5,7 +5,11 @@
5
5
 
6
6
  import fs from 'node:fs'
7
7
  import path from 'node:path'
8
- import { readResourceValues, resolveThemeAttribute } from './android-theme'
8
+ import {
9
+ frameworkColor,
10
+ readResourceValues,
11
+ resolveThemeAttribute,
12
+ } from './android-theme'
9
13
  import { parseXml, type XmlElement } from './android-xml'
10
14
  import frameworkThemes from './generated/android-framework-themes.json' with { type: 'json' }
11
15
 
@@ -18,10 +22,11 @@ export interface KotlinAppProject {
18
22
  namespace: string
19
23
  targetSdk: number
20
24
  // the launched activity's window as its theme and target SDK configure it;
21
- // statusBarColor is ARGB
25
+ // statusBarColor and navigationBarColor are ARGB
22
26
  window: {
23
27
  edgeToEdgeEnforced: boolean
24
28
  statusBarColor: number
29
+ navigationBarColor: number
25
30
  statusBarContrastEnforced: boolean
26
31
  navigationBarContrastEnforced: boolean
27
32
  lightStatusBar: boolean
@@ -221,9 +226,22 @@ function readWindowTheme(
221
226
  lightStatusBar: flag('windowLightStatusBar'),
222
227
  lightNavigationBar: flag('windowLightNavigationBar'),
223
228
  }
229
+ // the theme's navigation bar color; left at the framework's transparent
230
+ // default, a window that is not edge to edge takes the compatible color
231
+ // unless the device's config_navBarDefaultTransparent is set, which it is
232
+ // not on the pixel 8 android 17 device the oracle measures
233
+ const navigationColor = resolveThemeAttribute(name, 'navigationBarColor', sources)
234
+ if (typeof navigationColor === 'boolean')
235
+ throw new Error(`${name}: navigationBarColor is not a color`)
236
+ const navigationDefault = frameworkColor('navigation_bar_default', sources)
237
+ const navigationTheme = navigationColor ?? navigationDefault
238
+ const navigationBarColor =
239
+ navigationTheme === navigationDefault && !edgeToEdgeEnforced
240
+ ? frameworkColor('navigation_bar_compatible', sources)
241
+ : navigationTheme
224
242
  // PhoneWindow enforces edge to edge apart from whether the theme draws bar
225
243
  // backgrounds: either way the bar is transparent
226
- if (edgeToEdgeEnforced) return { ...window, statusBarColor: 0 }
244
+ if (edgeToEdgeEnforced) return { ...window, statusBarColor: 0, navigationBarColor }
227
245
  if (!flag('windowDrawsSystemBarBackgrounds'))
228
246
  throw new Error(
229
247
  `${name} does not draw system bar backgrounds; peach kotlin draws only theme status bar colors`,
@@ -231,7 +249,7 @@ function readWindowTheme(
231
249
  const color = resolveThemeAttribute(name, 'statusBarColor', sources)
232
250
  if (typeof color === 'boolean')
233
251
  throw new Error(`${name}: statusBarColor is not a color`)
234
- return { ...window, statusBarColor: color ?? 0xff000000 }
252
+ return { ...window, statusBarColor: color ?? 0xff000000, navigationBarColor }
235
253
  }
236
254
 
237
255
  function enabled(element: XmlElement): boolean {
@@ -337,13 +337,14 @@ export async function kotlinAppArtifact(
337
337
  '',
338
338
  '@JsExport',
339
339
  'fun peachAppMount(density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,',
340
- ' regular: String, medium: String, bold: String, statusBar: Int, navigationBar: Int, night: Boolean,',
340
+ ' regular: String, medium: String, bold: String, insets: String, night: Boolean,',
341
341
  ' invalidate: (Boolean) -> Unit): String = dev.peach.app.peachStartApp(',
342
342
  ' dev.peach.app.PeachAppSpec(',
343
343
  ` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.name)}),`,
344
344
  ` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.targetClass)}),`,
345
345
  ' android.view.WindowTheme(',
346
346
  ` ${app.window.edgeToEdgeEnforced}, 0x${app.window.statusBarColor.toString(16).padStart(8, '0')}.toInt(),`,
347
+ ` 0x${app.window.navigationBarColor.toString(16).padStart(8, '0')}.toInt(),`,
347
348
  ` ${app.window.statusBarContrastEnforced}, ${app.window.navigationBarContrastEnforced},`,
348
349
  ` ${app.window.lightStatusBar}, ${app.window.lightNavigationBar},`,
349
350
  ' ),',
@@ -351,7 +352,7 @@ export async function kotlinAppArtifact(
351
352
  ` { ${app.launcher.targetClass}() },`,
352
353
  ` ${app.handlesUiMode},`,
353
354
  ' ),',
354
- ' density, fontScale, locale, backend, stencilBits, regular, medium, bold, statusBar, navigationBar, night, invalidate,',
355
+ ' density, fontScale, locale, backend, stencilBits, regular, medium, bold, insets, night, invalidate,',
355
356
  ')',
356
357
  '',
357
358
  '@JsExport',