diff --git a/packages/app/src/components/app-diagnostic-sheet.tsx b/packages/app/src/components/app-diagnostic-sheet.tsx index 295910842..c82d7897c 100644 --- a/packages/app/src/components/app-diagnostic-sheet.tsx +++ b/packages/app/src/components/app-diagnostic-sheet.tsx @@ -1,19 +1,13 @@ import * as Clipboard from "expo-clipboard"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - Platform, - Pressable, - ScrollView, - Text, - View, - type PressableStateCallbackType, -} from "react-native"; +import { Platform, Pressable, Text, View, type PressableStateCallbackType } from "react-native"; import { Copy, RotateCw } from "lucide-react-native"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; import { useTranslation } from "react-i18next"; import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { ScrollableCodeSurface, SurfaceCard } from "@/components/ui/scrollable-code-surface"; import { useToast } from "@/contexts/toast-context"; import { getDesktopDaemonLogs, getDesktopDaemonStatus } from "@/desktop/daemon/desktop-daemon"; import { @@ -24,7 +18,6 @@ import { redactAppDiagnosticReport, } from "@/diagnostics/app-diagnostic-report"; import { getHostRuntimeStore, useHosts, type HostRuntimeSnapshot } from "@/runtime/host-runtime"; -import { settingsStyles } from "@/styles/settings"; import { ICON_SIZE, type Theme } from "@/styles/theme"; import type { HostProfile } from "@/types/host-connection"; @@ -230,16 +223,12 @@ export function AppDiagnosticSheet({ scrollable={false} testID="app-diagnostic-sheet" > - - {diagnostic ? ( - - - - {diagnostic} - - - - ) : ( + {diagnostic ? ( + + {diagnostic} + + ) : ( + {progress.length === 0 ? ( @@ -256,7 +245,7 @@ export function AppDiagnosticSheet({ /> ) : ( )} - )} - + + )} ); } @@ -374,22 +363,6 @@ function formatProgressStatus(status: ProgressStatus): string { } const styles = StyleSheet.create((theme) => ({ - diagnosticCard: { - overflow: "hidden", - }, - codeScroll: { - maxHeight: 520, - }, - codeContent: { - paddingVertical: theme.spacing[3], - paddingHorizontal: theme.spacing[4], - }, - codeText: { - fontFamily: theme.fontFamily.mono, - fontSize: theme.fontSize.code, - color: theme.colors.foreground, - lineHeight: 18, - }, progressContent: { paddingVertical: theme.spacing[4], paddingHorizontal: theme.spacing[4], @@ -431,9 +404,9 @@ const styles = StyleSheet.create((theme) => ({ backgroundColor: theme.colors.success, }, statusDotFailed: { + width: 8, + height: 8, + borderRadius: theme.borderRadius.full, backgroundColor: theme.colors.destructive, }, })); - -const DIAGNOSTIC_CARD_STYLE = [settingsStyles.card, styles.diagnosticCard]; -const FAILED_STATUS_DOT_STYLE = [styles.statusDot, styles.statusDotFailed]; diff --git a/packages/app/src/components/provider-diagnostic-sheet.tsx b/packages/app/src/components/provider-diagnostic-sheet.tsx index 5d9c19978..9085801e0 100644 --- a/packages/app/src/components/provider-diagnostic-sheet.tsx +++ b/packages/app/src/components/provider-diagnostic-sheet.tsx @@ -7,7 +7,6 @@ import { ActivityIndicator, Pressable, type PressableStateCallbackType, - ScrollView, Text, View, } from "react-native"; @@ -19,6 +18,7 @@ import { } from "@/components/adaptive-modal-sheet"; import { Button } from "@/components/ui/button"; import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { ScrollableCodeSurface, SurfaceCard } from "@/components/ui/scrollable-code-surface"; import { useIsCompactFormFactor } from "@/constants/layout"; import { isWeb } from "@/constants/platform"; import { useToast } from "@/contexts/toast-context"; @@ -363,26 +363,26 @@ function DiagnosticSubSheet({ let body: React.ReactNode; if (loading && !diagnostic) { body = ( - - - {t("settings.providers.diagnostic.running")} - + + + + {t("settings.providers.diagnostic.running")} + + ); } else if (diagnostic) { body = ( - - - - {diagnostic} - - - + + {diagnostic} + ); } else { body = ( - - {t("settings.providers.diagnostic.none")} - + + + {t("settings.providers.diagnostic.none")} + + ); } @@ -395,7 +395,7 @@ function DiagnosticSubSheet({ scrollable={false} testID="provider-diagnostic-sheet" > - {body} + {body} ); } @@ -862,22 +862,6 @@ const sheetStyles = StyleSheet.create((theme) => ({ justifyContent: "flex-end", gap: theme.spacing[2], }, - diagnosticCard: { - overflow: "hidden", - }, - codeScroll: { - maxHeight: 480, - }, - codeContent: { - paddingVertical: theme.spacing[3], - paddingHorizontal: theme.spacing[4], - }, - codeText: { - fontFamily: theme.fontFamily.mono, - fontSize: theme.fontSize.code, - color: theme.colors.foreground, - lineHeight: 18, - }, codeBlockLoading: { paddingVertical: theme.spacing[4], paddingHorizontal: theme.spacing[4], @@ -893,4 +877,3 @@ const COMPACT_FOOTER_META_STYLE = [sheetStyles.footerMeta, sheetStyles.compactFo const MAIN_SNAP_POINTS = ["65%", "92%"]; const ADD_SNAP_POINTS = ["40%"]; const DIAGNOSTIC_SNAP_POINTS = ["50%", "85%"]; -const DIAGNOSTIC_CARD_STYLE = [settingsStyles.card, sheetStyles.diagnosticCard]; diff --git a/packages/app/src/components/ui/scrollable-code-surface.tsx b/packages/app/src/components/ui/scrollable-code-surface.tsx new file mode 100644 index 000000000..ab900f9ac --- /dev/null +++ b/packages/app/src/components/ui/scrollable-code-surface.tsx @@ -0,0 +1,210 @@ +import { useMemo, type ReactNode } from "react"; +import { + ScrollView, + Text, + View, + type StyleProp, + type TextStyle, + type ViewStyle, +} from "react-native"; +import { StyleSheet } from "react-native-unistyles"; + +import { AppearanceStyleBoundary } from "@/components/appearance-style-boundary"; +import { isWeb } from "@/constants/platform"; +import { CODE_SURFACE_DATASET } from "@/styles/code-surface"; +import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style"; + +type CodeSurfaceTone = "surface0" | "surface1" | "surface2"; + +interface ScrollableCodeSurfaceProps { + children: ReactNode; + maxHeight?: number; + horizontal?: boolean; + selectable?: boolean; + tone?: CodeSurfaceTone; + bordered?: boolean; + style?: StyleProp; + scrollStyle?: StyleProp; + contentContainerStyle?: StyleProp; + textStyle?: StyleProp; + testID?: string; + accessibilityLabel?: string; +} + +interface SurfaceCardProps { + children: ReactNode; + tone?: CodeSurfaceTone; + bordered?: boolean; + style?: StyleProp; + testID?: string; + accessibilityLabel?: string; + codeSurface?: boolean; +} + +export function SurfaceCard({ + children, + tone = "surface1", + bordered = true, + style, + testID, + accessibilityLabel, + codeSurface = false, +}: SurfaceCardProps) { + const surfaceStyle = getSurfaceStyle(tone); + const containerStyle = useMemo( + () => [styles.container, surfaceStyle, bordered && styles.bordered, style], + [bordered, style, surfaceStyle], + ); + + const surface = ( + + {children} + + ); + return {surface}; +} + +export function ScrollableCodeSurface({ + children, + maxHeight, + horizontal = true, + selectable = true, + tone = "surface1", + bordered = true, + style, + scrollStyle, + contentContainerStyle, + textStyle, + testID, + accessibilityLabel, +}: ScrollableCodeSurfaceProps) { + const maxHeightStyle = useMemo( + () => (maxHeight == null ? null : inlineUnistylesStyle({ maxHeight })), + [maxHeight], + ); + const outerScrollStyle = useMemo( + () => [maxHeightStyle, scrollStyle], + [maxHeightStyle, scrollStyle], + ); + const contentStyle = useMemo( + () => [styles.content, contentContainerStyle], + [contentContainerStyle], + ); + const codeTextStyle = useMemo(() => [styles.text, textStyle], [textStyle]); + const codeContent = + typeof children === "string" ? ( + + ) : ( + + {children} + + ); + + return ( + + + {horizontal ? ( + + {codeContent} + + ) : ( + codeContent + )} + + + ); +} + +function PlainCodeText({ + text, + selectable, + textStyle, +}: { + text: string; + selectable: boolean; + textStyle: StyleProp; +}) { + const lines = useMemo(() => { + let offset = 0; + return text.split("\n").map((line) => { + const key = `${offset}:${line.length}`; + offset += line.length + 1; + return { key, line }; + }); + }, [text]); + return ( + + {lines.map(({ key, line }) => ( + + {line.length === 0 ? "\u200B" : line} + + ))} + + ); +} + +function getSurfaceStyle(tone: CodeSurfaceTone): StyleProp { + switch (tone) { + case "surface0": + return styles.surface0; + case "surface2": + return styles.surface2; + case "surface1": + return styles.surface1; + } +} + +const styles = StyleSheet.create((theme) => ({ + container: { + overflow: "hidden", + }, + bordered: { + borderRadius: theme.borderRadius.lg, + borderWidth: theme.borderWidth[1], + borderColor: theme.colors.border, + }, + surface0: { + backgroundColor: theme.colors.surface0, + }, + surface1: { + backgroundColor: theme.colors.surface1, + }, + surface2: { + backgroundColor: theme.colors.surface2, + }, + content: { + paddingVertical: theme.spacing[3], + paddingHorizontal: theme.spacing[4], + }, + text: { + backgroundColor: "transparent", + fontFamily: theme.fontFamily.mono, + fontSize: theme.fontSize.code, + color: theme.colors.foreground, + lineHeight: 18, + ...(isWeb + ? { + whiteSpace: "pre", + overflowWrap: "normal", + } + : null), + }, +}));