diff --git a/docs/UNISTYLES.md b/docs/UNISTYLES.md
index 8ed81e891..3d992d50b 100644
--- a/docs/UNISTYLES.md
+++ b/docs/UNISTYLES.md
@@ -89,6 +89,38 @@ const { theme } = useUnistyles();
Use this sparingly. It works because React re-renders the prop, but it gives up the main Unistyles native-update path for that value.
+## `withUnistyles` And The `> *` Child-Selector Leak
+
+`withUnistyles` on a component with a theme-dependent `style` prop works by wrapping the component in a `
` and emitting the style under a `.hash > *` child selector so the styles cascade onto the wrapped component. This is how auto-mapping for `style` and `contentContainerStyle` works on web.
+
+The sharp edge: Unistyles hashes styles by value. If `withUnistyles` receives a style whose value is **identical** to a style used elsewhere in the app on a plain `View`, both usages get the same hash — and both CSS rules (the element rule and the `> *` child rule) are emitted under the same class name. The `> *` rule then leaks onto the direct children of every `View` that shares the hash.
+
+Concrete regression we hit: `welcome-screen.tsx` had `const ThemedScrollView = withUnistyles(ScrollView)` with `style={{ flex: 1, backgroundColor: theme.colors.surface0 }}`. `agent-panel.tsx` had `root` and `container` styles with the exact same value. All three collided on class `unistyles_j2k2iilhfz`, so the browser stylesheet contained:
+
+```css
+.unistyles_j2k2iilhfz { flex: 1 1 0%; background-color: var(--colors-surface0); }
+.unistyles_j2k2iilhfz > * { flex: 1 1 0%; background-color: var(--colors-surface0); }
+```
+
+The child-selector rule forced `flex:1` and `background-color: surface0` onto the Composer's outer `Animated.View` (a direct child of `container`), stretching it to fill remaining space and leaving a large empty gap between the composer UI and the bottom of the screen. It also painted a `surface0` band behind the scroll-to-bottom button. The bug only appeared in the browser — Electron skips `WelcomeScreen` after pairing, so the `> *` rule was never injected there.
+
+Symptoms to watch for:
+
+- A sibling of a themed panel-background `View` stretches unexpectedly on web only.
+- Random direct children of a `{ flex: 1, backgroundColor: surface0 }` `View` pick up an unexpected background.
+- DevTools shows a `.unistyles_xxx > *` rule you did not write.
+
+Quick confirmation in DevTools console:
+
+```js
+[...document.styleSheets].flatMap(s => [...(s.cssRules || [])])
+ .map(r => r.cssText).filter(t => t.includes("unistyles") && t.includes("> *"));
+```
+
+Any match beyond benign `r-pointerEvents-* > *` rules from react-native-web is a leak.
+
+Avoid the bug by preferring the wrapper-`View` pattern from the previous section whenever possible: put `{ flex: 1, backgroundColor: surface0 }` on a plain `View` and give the `ScrollView` a theme-free `style`/`contentContainerStyle`. That keeps `withUnistyles` off the hot path and avoids the hash collision. Only reach for `withUnistyles(ScrollView)` when a wrapper view is genuinely awkward, and when you do, give the wrapped style a distinctive shape (extra key, different layout) so it does not hash-collide with a common panel background used elsewhere.
+
## Hidden Sheet Content
`@gorhom/bottom-sheet` can keep `BottomSheetModal` content mounted while the sheet is hidden. That matters during Paseo's startup theme transition: a header node can be created under the initial adaptive theme, stay hidden, then appear later with stale native style values even though surrounding content has re-rendered correctly.
diff --git a/packages/app/src/components/welcome-screen.tsx b/packages/app/src/components/welcome-screen.tsx
index e431b1341..56677f5ee 100644
--- a/packages/app/src/components/welcome-screen.tsx
+++ b/packages/app/src/components/welcome-screen.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
import { Pressable, Text, View, ScrollView } from "react-native";
import { useRouter } from "expo-router";
-import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles";
+import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { QrCode, Link2, ClipboardPaste, ExternalLink, Settings } from "lucide-react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { HostProfile } from "@/types/host-connection";
@@ -21,8 +21,6 @@ import { PaseoLogo } from "@/components/icons/paseo-logo";
import { openExternalUrl } from "@/utils/open-external-url";
import { isWeb, isNative } from "@/constants/platform";
-const ThemedScrollView = withUnistyles(ScrollView);
-
type WelcomeAction = {
key: "scan-qr" | "direct-connection" | "paste-pairing-link";
label: string;
@@ -33,13 +31,15 @@ type WelcomeAction = {
};
const styles = StyleSheet.create((theme) => ({
- scrollView: {
+ root: {
flex: 1,
backgroundColor: theme.colors.surface0,
},
+ scrollView: {
+ flex: 1,
+ },
container: {
flexGrow: 1,
- backgroundColor: theme.colors.surface0,
padding: theme.spacing[6],
paddingBottom: 0,
alignItems: "center",
@@ -309,97 +309,101 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
const showHostList = hosts.length > 0 && !anyOnlineServerId;
return (
-
-
-
-
- Welcome to Paseo
- {showHostList ? (
- Connecting to your hosts…
- ) : (
- <>
- Connect your computer to get started
- {isNative ? (
- openExternalUrl("https://paseo.sh")}
- >
- paseo.sh
-
-
- ) : null}
- >
- )}
-
-
-
- {actions.map((action) => {
- const Icon = action.icon;
- return (
-
-
-
- {action.label}
-
-
- );
- })}
-
-
- {showHostList && (
-
- {hosts.map((host) => (
-
- ))}
+
+
+
+
+
+ Welcome to Paseo
+ {showHostList ? (
+ Connecting to your hosts…
+ ) : (
+ <>
+ Connect your computer to get started
+ {isNative ? (
+ openExternalUrl("https://paseo.sh")}
+ >
+ paseo.sh
+
+
+ ) : null}
+ >
+ )}
- )}
-
-
- {appVersionText}
+
+ {actions.map((action) => {
+ const Icon = action.icon;
+ return (
+
+
+
+ {action.label}
+
+
+ );
+ })}
+
- setIsDirectOpen(false)}
- onSaved={({ profile, serverId }) => {
- onHostAdded?.(profile);
- finishOnboarding(serverId);
- }}
- />
+ {showHostList && (
+
+ {hosts.map((host) => (
+
+ ))}
+
+ )}
- setIsPasteLinkOpen(false)}
- onSaved={({ profile, serverId }) => {
- onHostAdded?.(profile);
- finishOnboarding(serverId);
- }}
- />
-
+
+
+ {appVersionText}
+
+ setIsDirectOpen(false)}
+ onSaved={({ profile, serverId }) => {
+ onHostAdded?.(profile);
+ finishOnboarding(serverId);
+ }}
+ />
+
+ setIsPasteLinkOpen(false)}
+ onSaved={({ profile, serverId }) => {
+ onHostAdded?.(profile);
+ finishOnboarding(serverId);
+ }}
+ />
+
+
);
}