style(app): theme native scrollbars across all web views (#399)

* style(app): theme native scrollbars in tool call detail views

Apply CSS scrollbar-color to all ScrollViews in ToolCallDetailsContent
and DiffViewer so the browser scrollbar matches the app's dark theme
instead of showing the default white native scrollbar.

* refactor: split useWebScrollbarStyle into .web.ts/.native.ts platform files

- Native shim returns undefined without calling useUnistyles
- Web variant uses properly typed WebScrollbarStyle interface instead of `as any`
- Add .d.ts for TypeScript module resolution

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
amir
2026-04-16 03:59:31 +01:00
committed by GitHub
parent 86bb5cc827
commit a1f3256923
11 changed files with 95 additions and 11 deletions

View File

@@ -2,6 +2,7 @@ import { View, Text, ScrollView, Pressable, Modal } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { StyleSheet } from "react-native-unistyles";
import { Fonts } from "@/constants/theme";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
export interface Artifact {
id: string;
@@ -147,6 +148,8 @@ const styles = StyleSheet.create((theme) => ({
}));
export function ArtifactDrawer({ artifact, onClose }: ArtifactDrawerProps) {
const webScrollbarStyle = useWebScrollbarStyle();
if (!artifact) {
return null;
}
@@ -190,7 +193,7 @@ export function ArtifactDrawer({ artifact, onClose }: ArtifactDrawerProps) {
</View>
{/* Content */}
<ScrollView
style={styles.contentScroll}
style={[styles.contentScroll, webScrollbarStyle]}
contentContainerStyle={styles.contentScrollContainer}
>
{artifact.type === "image" ? (
@@ -200,7 +203,11 @@ export function ArtifactDrawer({ artifact, onClose }: ArtifactDrawerProps) {
</View>
) : (
<View style={styles.codeContainer}>
<ScrollView horizontal showsHorizontalScrollIndicator={true}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={true}
style={webScrollbarStyle}
>
<Text style={styles.codeText}>{content}</Text>
</ScrollView>
</View>

View File

@@ -1,4 +1,5 @@
import { ScrollView, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
interface DiffScrollProps {
children: React.ReactNode;
@@ -14,12 +15,14 @@ export function DiffScroll({
style,
contentContainerStyle,
}: DiffScrollProps) {
const webScrollbarStyle = useWebScrollbarStyle();
return (
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
style={style}
style={[style, webScrollbarStyle]}
contentContainerStyle={contentContainerStyle}
onLayout={(e: LayoutChangeEvent) => onScrollViewWidthChange(e.nativeEvent.layout.width)}
>

View File

@@ -4,6 +4,7 @@ import { ScrollView as GHScrollView } from "react-native-gesture-handler";
import { StyleSheet } from "react-native-unistyles";
import { Fonts } from "@/constants/theme";
import type { DiffLine, DiffSegment } from "@/utils/tool-call-parsers";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
import { getCodeInsets } from "./code-insets";
import { isWeb } from "@/constants/platform";
@@ -23,6 +24,7 @@ export function DiffViewer({
fillAvailableHeight = false,
}: DiffViewerProps) {
const [scrollViewWidth, setScrollViewWidth] = React.useState(0);
const webScrollbarStyle = useWebScrollbarStyle();
if (!diffLines.length) {
return (
@@ -38,6 +40,7 @@ export function DiffViewer({
styles.verticalScroll,
maxHeight !== undefined && { maxHeight },
fillAvailableHeight && styles.fillHeight,
webScrollbarStyle,
]}
contentContainerStyle={styles.verticalContent}
nestedScrollEnabled
@@ -47,6 +50,7 @@ export function DiffViewer({
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
style={webScrollbarStyle}
contentContainerStyle={styles.horizontalContent}
onLayout={(e) => setScrollViewWidth(e.nativeEvent.layout.width)}
>

View File

@@ -13,6 +13,7 @@ import { useIsCompactFormFactor } from "@/constants/layout";
import { Fonts } from "@/constants/theme";
import { useSessionStore, type ExplorerFile } from "@/stores/session-store";
import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
import {
highlightCode,
darkHighlightColors,
@@ -125,6 +126,7 @@ function FilePreviewBody({
const isMarkdownFile = preview?.kind === "text" && isRenderedMarkdownFile(filePath);
const previewScrollRef = useRef<RNScrollView>(null);
const webScrollbarStyle = useWebScrollbarStyle();
const scrollbar = useWebScrollViewScrollbar(previewScrollRef, {
enabled: showDesktopWebScrollbar,
});
@@ -216,6 +218,7 @@ function FilePreviewBody({
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
style={webScrollbarStyle}
contentContainerStyle={styles.previewCodeScrollContent}
>
{codeLines}

View File

@@ -6,6 +6,7 @@ import { Fonts } from "@/constants/theme";
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
import { buildLineDiff, parseUnifiedDiff } from "@/utils/tool-call-parsers";
import { hasMeaningfulToolCallDetail } from "@/utils/tool-call-detail-state";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
import { DiffViewer } from "./diff-viewer";
import { getCodeInsets } from "./code-insets";
import { isWeb } from "@/constants/platform";
@@ -30,6 +31,7 @@ export function ToolCallDetailsContent({
showLoadingSkeleton = false,
}: ToolCallDetailsContentProps) {
const resolvedMaxHeight = fillAvailableHeight ? undefined : (maxHeight ?? 300);
const webScrollbarStyle = useWebScrollbarStyle();
// Compute diff lines for edit type
const diffLines = useMemo(() => {
@@ -65,6 +67,7 @@ export function ToolCallDetailsContent({
styles.codeVerticalScroll,
resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight },
shouldFill && styles.fillHeight,
webScrollbarStyle,
]}
contentContainerStyle={styles.codeVerticalContent}
nestedScrollEnabled
@@ -74,6 +77,7 @@ export function ToolCallDetailsContent({
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
style={webScrollbarStyle}
contentContainerStyle={styles.codeHorizontalContent}
>
<View style={styles.codeLine}>
@@ -99,6 +103,7 @@ export function ToolCallDetailsContent({
styles.codeVerticalScroll,
resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight },
shouldFill && styles.fillHeight,
webScrollbarStyle,
]}
contentContainerStyle={styles.codeVerticalContent}
nestedScrollEnabled
@@ -108,6 +113,7 @@ export function ToolCallDetailsContent({
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
style={webScrollbarStyle}
contentContainerStyle={styles.codeHorizontalContent}
>
<View style={styles.codeLine}>
@@ -137,6 +143,7 @@ export function ToolCallDetailsContent({
styles.codeVerticalScroll,
resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight },
shouldFill && styles.fillHeight,
webScrollbarStyle,
]}
contentContainerStyle={styles.codeVerticalContent}
nestedScrollEnabled
@@ -146,6 +153,7 @@ export function ToolCallDetailsContent({
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
style={webScrollbarStyle}
contentContainerStyle={styles.codeHorizontalContent}
>
<View style={styles.codeLine}>
@@ -181,12 +189,18 @@ export function ToolCallDetailsContent({
styles.scrollArea,
resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight },
shouldFill && styles.fillHeight,
webScrollbarStyle,
]}
contentContainerStyle={styles.scrollContent}
nestedScrollEnabled
showsVerticalScrollIndicator={true}
>
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator={true}>
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator={true}
style={webScrollbarStyle}
>
<Text selectable style={styles.scrollText}>
{detail.content}
</Text>
@@ -204,12 +218,18 @@ export function ToolCallDetailsContent({
styles.scrollArea,
resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight },
shouldFill && styles.fillHeight,
webScrollbarStyle,
]}
contentContainerStyle={styles.scrollContent}
nestedScrollEnabled
showsVerticalScrollIndicator={true}
>
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator={true}>
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator={true}
style={webScrollbarStyle}
>
<Text selectable style={styles.scrollText}>
{detail.content}
</Text>
@@ -236,12 +256,18 @@ export function ToolCallDetailsContent({
style={[
styles.scrollArea,
resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight },
webScrollbarStyle,
]}
contentContainerStyle={styles.scrollContent}
nestedScrollEnabled
showsVerticalScrollIndicator
>
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator>
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
style={webScrollbarStyle}
>
<Text selectable style={styles.scrollText}>
{detail.content}
</Text>
@@ -286,12 +312,18 @@ export function ToolCallDetailsContent({
styles.scrollArea,
resolvedMaxHeight !== undefined && { maxHeight: resolvedMaxHeight },
shouldFill && styles.fillHeight,
webScrollbarStyle,
]}
contentContainerStyle={styles.scrollContent}
nestedScrollEnabled
showsVerticalScrollIndicator
>
<ScrollView horizontal nestedScrollEnabled showsHorizontalScrollIndicator>
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
style={webScrollbarStyle}
>
<Text selectable style={styles.scrollText}>
{detail.result ? `${detail.url}\n\n${detail.result}` : detail.url}
</Text>
@@ -356,7 +388,7 @@ export function ToolCallDetailsContent({
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
style={[styles.jsonScroll, webScrollbarStyle]}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
@@ -378,7 +410,7 @@ export function ToolCallDetailsContent({
<ScrollView
horizontal
nestedScrollEnabled
style={[styles.jsonScroll, styles.jsonScrollError]}
style={[styles.jsonScroll, styles.jsonScrollError, webScrollbarStyle]}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>

View File

@@ -33,6 +33,7 @@ import { Check, CheckCircle } from "lucide-react-native";
import { BottomSheetBackdrop, BottomSheetModal, BottomSheetScrollView } from "@gorhom/bottom-sheet";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { isWeb, isNative } from "@/constants/platform";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
// Keep parity with dropdown-menu action statuses.
export type ActionStatus = "idle" | "pending" | "success";
@@ -348,6 +349,7 @@ export function ContextMenuContent({
testID?: string;
}>): ReactElement | null {
const context = useContextMenuContext("ContextMenuContent");
const webScrollbarStyle = useWebScrollbarStyle();
const isMobile = useIsCompactFormFactor();
const useMobileSheet = isMobile && mobileMode === "sheet";
const { open, setOpen, triggerRef, anchorRect } = context;
@@ -537,6 +539,7 @@ export function ContextMenuContent({
<ScrollView
bounces={false}
showsVerticalScrollIndicator
style={webScrollbarStyle}
contentContainerStyle={{ flexGrow: 1 }}
>
{children}

View File

@@ -28,6 +28,7 @@ import Animated, { Keyframe, runOnJS } from "react-native-reanimated";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { Check, CheckCircle } from "lucide-react-native";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
// Action status for menu items with loading/success feedback
export type ActionStatus = "idle" | "pending" | "success";
@@ -267,6 +268,7 @@ export function DropdownMenuContent({
}>): ReactElement | null {
const { open, setOpen, triggerRef } = useDropdownMenuContext("DropdownMenuContent");
const [modalVisible, setModalVisible] = useState(false);
const webScrollbarStyle = useWebScrollbarStyle();
const [closing, setClosing] = useState(false);
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
@@ -411,6 +413,7 @@ export function DropdownMenuContent({
<ScrollView
bounces={false}
showsVerticalScrollIndicator
style={webScrollbarStyle}
contentContainerStyle={{ flexGrow: 1 }}
>
{children}

View File

@@ -0,0 +1 @@
export * from "./use-web-scrollbar-style.web";

View File

@@ -0,0 +1,5 @@
import type { ViewStyle } from "react-native";
export function useWebScrollbarStyle(): ViewStyle | undefined {
return undefined;
}

View File

@@ -0,0 +1,21 @@
import { useMemo } from "react";
import type { ViewStyle } from "react-native";
import { useUnistyles } from "react-native-unistyles";
// CSS scrollbar properties are supported by React Native Web at runtime
// but are not included in React Native's ViewStyle type definition.
interface WebScrollbarStyle extends ViewStyle {
scrollbarColor: string;
scrollbarWidth: string;
}
export function useWebScrollbarStyle(): WebScrollbarStyle {
const { theme } = useUnistyles();
return useMemo(
(): WebScrollbarStyle => ({
scrollbarColor: `${theme.colors.scrollbarHandle} transparent`,
scrollbarWidth: "thin",
}),
[theme.colors.scrollbarHandle],
);
}

View File

@@ -10,6 +10,7 @@ import { Fonts } from "@/constants/theme";
import { getDesktopDaemonLogs, type DesktopDaemonLogs } from "@/desktop/daemon/desktop-daemon";
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
import { isWeb } from "@/constants/platform";
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
type StartupSplashScreenProps = {
bootstrapState?: {
@@ -161,6 +162,7 @@ const styles = StyleSheet.create((theme) => ({
export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps) {
const { theme } = useUnistyles();
const webScrollbarStyle = useWebScrollbarStyle();
const [daemonLogs, setDaemonLogs] = useState<DesktopDaemonLogs | null>(null);
const [logsError, setLogsError] = useState<string | null>(null);
const [isLoadingLogs, setIsLoadingLogs] = useState(false);
@@ -282,7 +284,7 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps
<View style={styles.errorScreen}>
<TitlebarDragRegion />
<ScrollView
style={styles.errorScrollView}
style={[styles.errorScrollView, webScrollbarStyle]}
contentContainerStyle={styles.errorScrollContent}
showsVerticalScrollIndicator
>
@@ -303,7 +305,7 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps
<View style={styles.logsContainer}>
<ScrollView
style={styles.logsScroll}
style={[styles.logsScroll, webScrollbarStyle]}
contentContainerStyle={styles.logsContent}
showsVerticalScrollIndicator
>