Merge branch 'config-work' into dev
This commit is contained in:
@@ -85,7 +85,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.lg,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
},
|
||||
@@ -251,7 +250,7 @@ export function AdaptiveModalSheet({
|
||||
>
|
||||
<View style={styles.bottomSheetHeader} testID={testID}>
|
||||
<View style={styles.headerTitleGroup}>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
<Text style={[styles.title, { color: theme.colors.foreground }]} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle}
|
||||
@@ -280,7 +279,7 @@ export function AdaptiveModalSheet({
|
||||
<>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerTitleGroup}>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
<Text style={[styles.title, { color: theme.colors.foreground }]} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle}
|
||||
|
||||
@@ -70,6 +70,7 @@ export function AddHostMethodModal({
|
||||
<Pressable
|
||||
style={styles.option}
|
||||
onPress={handleDirect}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Direct connection"
|
||||
testID="add-host-method-direct"
|
||||
>
|
||||
@@ -81,7 +82,12 @@ export function AddHostMethodModal({
|
||||
</Pressable>
|
||||
|
||||
{isNative ? (
|
||||
<Pressable style={styles.option} onPress={handleScan} accessibilityLabel="Scan QR code">
|
||||
<Pressable
|
||||
style={styles.option}
|
||||
onPress={handleScan}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Scan QR code"
|
||||
>
|
||||
<QrCode size={18} color={theme.colors.foreground} />
|
||||
<View style={styles.optionBody}>
|
||||
<Text style={styles.optionText}>Scan QR code</Text>
|
||||
@@ -93,6 +99,7 @@ export function AddHostMethodModal({
|
||||
<Pressable
|
||||
style={styles.option}
|
||||
onPress={handlePaste}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Paste pairing link"
|
||||
testID="add-host-method-pair-link"
|
||||
>
|
||||
|
||||
@@ -132,7 +132,6 @@ function buildConnectionFailureCopy(
|
||||
export interface AddHostModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
targetServerId?: string;
|
||||
onCancel?: () => void;
|
||||
onSaved?: (result: {
|
||||
profile: HostProfile;
|
||||
@@ -142,13 +141,7 @@ export interface AddHostModalProps {
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export function AddHostModal({
|
||||
visible,
|
||||
onClose,
|
||||
onCancel,
|
||||
onSaved,
|
||||
targetServerId,
|
||||
}: AddHostModalProps) {
|
||||
export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostModalProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const daemons = useHosts();
|
||||
const { upsertDirectConnection } = useHostMutations();
|
||||
@@ -211,15 +204,6 @@ export function AddHostModal({
|
||||
endpoint,
|
||||
});
|
||||
await client.close().catch(() => undefined);
|
||||
if (targetServerId && serverId !== targetServerId) {
|
||||
const message = `That endpoint belongs to ${serverId}, not ${targetServerId}.`;
|
||||
setErrorMessage(message);
|
||||
if (!isMobile) {
|
||||
Alert.alert("Wrong daemon", message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isNewHost = !daemons.some((daemon) => daemon.serverId === serverId);
|
||||
const profile = await upsertDirectConnection({
|
||||
serverId,
|
||||
@@ -245,7 +229,7 @@ export function AddHostModal({
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [daemons, handleClose, isMobile, isSaving, onSaved, targetServerId, upsertDirectConnection]);
|
||||
}, [daemons, handleClose, isMobile, isSaving, onSaved, upsertDirectConnection]);
|
||||
|
||||
return (
|
||||
<AdaptiveModalSheet
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { ChevronDown, GitBranch } from "lucide-react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
@@ -8,6 +8,7 @@ import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { useBranchSwitcher } from "@/hooks/use-branch-switcher";
|
||||
import { ScreenTitle } from "@/components/headers/screen-title";
|
||||
|
||||
interface BranchSwitcherProps {
|
||||
currentBranchName: string | null;
|
||||
@@ -46,9 +47,7 @@ export function BranchSwitcher({
|
||||
const titleContent = (
|
||||
<>
|
||||
{isGitCheckout ? <GitBranch size={14} color={theme.colors.foregroundMuted} /> : null}
|
||||
<Text testID="workspace-header-title" style={styles.headerTitle} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
<ScreenTitle testID="workspace-header-title">{title}</ScreenTitle>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -102,15 +101,6 @@ export function BranchSwitcher({
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
headerTitle: {
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: {
|
||||
xs: "400",
|
||||
md: "300",
|
||||
},
|
||||
color: theme.colors.foreground,
|
||||
flexShrink: 1,
|
||||
},
|
||||
branchSwitcherTrigger: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Pressable, Text } from "react-native";
|
||||
import { Pressable } from "react-native";
|
||||
import { router } from "expo-router";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { ArrowLeft } from "lucide-react-native";
|
||||
import { ScreenHeader } from "./screen-header";
|
||||
import { ScreenTitle } from "./screen-title";
|
||||
|
||||
interface BackHeaderProps {
|
||||
title?: string;
|
||||
titleAccessory?: ReactNode;
|
||||
rightContent?: ReactNode;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
export function BackHeader({ title, rightContent, onBack }: BackHeaderProps) {
|
||||
export function BackHeader({ title, titleAccessory, rightContent, onBack }: BackHeaderProps) {
|
||||
const { theme } = useUnistyles();
|
||||
|
||||
return (
|
||||
<ScreenHeader
|
||||
left={
|
||||
<>
|
||||
<Pressable onPress={onBack ?? (() => router.back())} style={styles.backButton}>
|
||||
<Pressable
|
||||
onPress={onBack ?? (() => router.back())}
|
||||
style={styles.backButton}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Back"
|
||||
>
|
||||
<ArrowLeft size={theme.iconSize.lg} color={theme.colors.foregroundMuted} />
|
||||
</Pressable>
|
||||
{title && (
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{title && <ScreenTitle>{title}</ScreenTitle>}
|
||||
{titleAccessory}
|
||||
</>
|
||||
}
|
||||
right={rightContent}
|
||||
@@ -45,13 +49,4 @@ const styles = StyleSheet.create((theme) => ({
|
||||
},
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
fontSize: theme.fontSize.lg,
|
||||
fontWeight: {
|
||||
xs: theme.fontWeight.semibold,
|
||||
md: "400",
|
||||
},
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { View } from "react-native";
|
||||
import { headerIconSlotStyle } from "./header-toggle-button";
|
||||
|
||||
/**
|
||||
* Non-interactive icon slot sitting at the start of a screen header's left
|
||||
* cluster. Shares the same padding + border-radius as `HeaderToggleButton` so
|
||||
* decorative headers (settings sections, host detail) line up with the sidebar
|
||||
* toggle across screens.
|
||||
*/
|
||||
export function HeaderIconBadge({ children }: { children: ReactNode }) {
|
||||
return <View style={headerIconSlotStyle.slot}>{children}</View>;
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export function HeaderToggleButton({
|
||||
onPress={(e) => {
|
||||
onPress(e);
|
||||
}}
|
||||
style={[styles.button, style]}
|
||||
style={[headerIconSlotStyle.slot, style]}
|
||||
>
|
||||
{typeof children === "function"
|
||||
? (state: { pressed: boolean; hovered?: boolean }) =>
|
||||
@@ -68,14 +68,17 @@ export function HeaderToggleButton({
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
button: {
|
||||
export const headerIconSlotStyle = StyleSheet.create((theme) => ({
|
||||
slot: {
|
||||
padding: {
|
||||
xs: theme.spacing[3],
|
||||
md: theme.spacing[2],
|
||||
},
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
},
|
||||
}));
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
tooltipRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Text, View, type StyleProp, type ViewStyle } from "react-native";
|
||||
import { View, type StyleProp, type ViewStyle } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { PanelLeft } from "lucide-react-native";
|
||||
import { ScreenHeader } from "./screen-header";
|
||||
import { ScreenTitle } from "./screen-title";
|
||||
import { HeaderToggleButton } from "./header-toggle-button";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
@@ -83,11 +84,7 @@ export function MenuHeader({ title, rightContent, borderless }: MenuHeaderProps)
|
||||
left={
|
||||
<>
|
||||
<SidebarMenuToggle />
|
||||
{title && (
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{title && <ScreenTitle>{title}</ScreenTitle>}
|
||||
</>
|
||||
}
|
||||
right={rightContent}
|
||||
@@ -101,15 +98,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
left: {
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
title: {
|
||||
flex: 1,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: {
|
||||
xs: "400",
|
||||
md: "300",
|
||||
},
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
mobileMenuIcon: {
|
||||
width: MOBILE_MENU_LINE_WIDTH,
|
||||
height: 12,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Text, type StyleProp, type TextStyle } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
|
||||
interface ScreenTitleProps {
|
||||
children: ReactNode;
|
||||
numberOfLines?: number;
|
||||
testID?: string;
|
||||
style?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical screen title for use inside `ScreenHeader`. One typography, one
|
||||
* color, responsive weight. Leading icons are siblings (HeaderToggleButton,
|
||||
* HeaderIconBadge) — never nested inside this component.
|
||||
*/
|
||||
export function ScreenTitle({ children, numberOfLines = 1, testID, style }: ScreenTitleProps) {
|
||||
return (
|
||||
<Text style={[styles.text, style]} numberOfLines={numberOfLines} testID={testID}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
text: {
|
||||
flexShrink: 1,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: {
|
||||
xs: "400",
|
||||
md: "300",
|
||||
},
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
}));
|
||||
@@ -33,6 +33,7 @@ import { Shortcut } from "@/components/ui/shortcut";
|
||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import { usePanelStore, MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH } from "@/stores/panel-store";
|
||||
import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
|
||||
import { SidebarWorkspaceList } from "./sidebar-workspace-list";
|
||||
import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton";
|
||||
import { useSidebarShortcutModel } from "@/hooks/use-sidebar-shortcut-model";
|
||||
@@ -46,14 +47,10 @@ import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
||||
import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox";
|
||||
import { useHostRuntimeSnapshot, useHosts } from "@/runtime/host-runtime";
|
||||
import { formatConnectionStatus } from "@/utils/daemons";
|
||||
import {
|
||||
HEADER_INNER_HEIGHT,
|
||||
HEADER_INNER_HEIGHT_MOBILE,
|
||||
useIsCompactFormFactor,
|
||||
} from "@/constants/layout";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import {
|
||||
buildHostSessionsRoute,
|
||||
buildHostSettingsRoute,
|
||||
buildSettingsRoute,
|
||||
mapPathnameToServer,
|
||||
parseServerIdFromPathname,
|
||||
} from "@/utils/host-routes";
|
||||
@@ -220,19 +217,13 @@ export const LeftSidebar = memo(function LeftSidebar({
|
||||
}, [openProjectPicker]);
|
||||
|
||||
const handleSettingsMobile = useCallback(() => {
|
||||
if (!activeServerId) {
|
||||
return;
|
||||
}
|
||||
closeToAgent();
|
||||
router.push(buildHostSettingsRoute(activeServerId));
|
||||
}, [activeServerId, closeToAgent]);
|
||||
router.push(buildSettingsRoute());
|
||||
}, [closeToAgent]);
|
||||
|
||||
const handleSettingsDesktop = useCallback(() => {
|
||||
if (!activeServerId) {
|
||||
return;
|
||||
}
|
||||
router.push(buildHostSettingsRoute(activeServerId));
|
||||
}, [activeServerId]);
|
||||
router.push(buildSettingsRoute());
|
||||
}, []);
|
||||
|
||||
const handleViewMoreNavigate = useCallback(() => {
|
||||
if (!activeServerId) {
|
||||
@@ -328,44 +319,6 @@ function HostSwitchOption({
|
||||
);
|
||||
}
|
||||
|
||||
function SessionsButton({ onPress }: { onPress: () => void }) {
|
||||
const { theme } = useUnistyles();
|
||||
const pathname = usePathname();
|
||||
const isActive = pathname.includes("/sessions");
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={({ hovered }) => [
|
||||
styles.newAgentButton,
|
||||
hovered && styles.newAgentButtonHovered,
|
||||
isActive && styles.newAgentButtonActive,
|
||||
]}
|
||||
testID="sidebar-sessions"
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Sessions"
|
||||
onPress={onPress}
|
||||
>
|
||||
{({ hovered }) => (
|
||||
<>
|
||||
<MessagesSquare
|
||||
size={theme.iconSize.md}
|
||||
color={hovered || isActive ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.newAgentButtonText,
|
||||
(hovered || isActive) && styles.newAgentButtonTextHovered,
|
||||
]}
|
||||
>
|
||||
Sessions
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileSidebar({
|
||||
theme,
|
||||
activeServerId,
|
||||
@@ -394,6 +347,8 @@ function MobileSidebar({
|
||||
handleViewMoreNavigate,
|
||||
}: MobileSidebarProps) {
|
||||
const newAgentKeys = useShortcutKeys("new-agent");
|
||||
const pathname = usePathname();
|
||||
const isSessionsActive = pathname.includes("/sessions");
|
||||
const {
|
||||
translateX,
|
||||
backdropOpacity,
|
||||
@@ -544,11 +499,13 @@ function MobileSidebar({
|
||||
pointerEvents="auto"
|
||||
>
|
||||
<View style={styles.sidebarContent} pointerEvents="auto">
|
||||
<View style={styles.sidebarHeader}>
|
||||
<View style={styles.sidebarHeaderRow}>
|
||||
<SessionsButton onPress={handleViewMore} />
|
||||
</View>
|
||||
</View>
|
||||
<SidebarHeaderRow
|
||||
icon={MessagesSquare}
|
||||
label="Sessions"
|
||||
onPress={handleViewMore}
|
||||
isActive={isSessionsActive}
|
||||
testID="sidebar-sessions"
|
||||
/>
|
||||
|
||||
{isInitialLoad ? (
|
||||
<SidebarAgentListSkeleton />
|
||||
@@ -676,6 +633,8 @@ function DesktopSidebar({
|
||||
handleViewMore,
|
||||
}: DesktopSidebarProps) {
|
||||
const newAgentKeys = useShortcutKeys("new-agent");
|
||||
const pathname = usePathname();
|
||||
const isSessionsActive = pathname.includes("/sessions");
|
||||
const padding = useWindowControlsPadding("sidebar");
|
||||
const sidebarWidth = usePanelStore((state) => state.sidebarWidth);
|
||||
const setSidebarWidth = usePanelStore((state) => state.setSidebarWidth);
|
||||
@@ -732,11 +691,13 @@ function DesktopSidebar({
|
||||
<View style={styles.sidebarDragArea}>
|
||||
<TitlebarDragRegion />
|
||||
{padding.top > 0 ? <View style={{ height: padding.top }} /> : null}
|
||||
<View style={styles.sidebarHeader}>
|
||||
<View style={styles.sidebarHeaderRow}>
|
||||
<SessionsButton onPress={handleViewMore} />
|
||||
</View>
|
||||
</View>
|
||||
<SidebarHeaderRow
|
||||
icon={MessagesSquare}
|
||||
label="Sessions"
|
||||
onPress={handleViewMore}
|
||||
isActive={isSessionsActive}
|
||||
testID="sidebar-sessions"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{isInitialLoad ? (
|
||||
@@ -881,46 +842,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
sidebarDragArea: {
|
||||
position: "relative",
|
||||
},
|
||||
sidebarHeader: {
|
||||
height: {
|
||||
xs: HEADER_INNER_HEIGHT_MOBILE,
|
||||
md: HEADER_INNER_HEIGHT,
|
||||
},
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
justifyContent: "center",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
userSelect: "none",
|
||||
},
|
||||
sidebarHeaderRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
newAgentButton: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
},
|
||||
newAgentButtonText: {
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
newAgentButtonTextHovered: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
newAgentButtonHovered: {
|
||||
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||
},
|
||||
newAgentButtonActive: {
|
||||
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||
},
|
||||
hostTrigger: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
||||
@@ -47,7 +47,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
export interface PairLinkModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
targetServerId?: string;
|
||||
onCancel?: () => void;
|
||||
onSaved?: (result: {
|
||||
profile: HostProfile;
|
||||
@@ -57,13 +56,7 @@ export interface PairLinkModalProps {
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export function PairLinkModal({
|
||||
visible,
|
||||
onClose,
|
||||
onCancel,
|
||||
onSaved,
|
||||
targetServerId,
|
||||
}: PairLinkModalProps) {
|
||||
export function PairLinkModal({ visible, onClose, onCancel, onSaved }: PairLinkModalProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const daemons = useHosts();
|
||||
const { upsertConnectionFromOfferUrl: upsertDaemonFromOfferUrl } = useHostMutations();
|
||||
@@ -128,15 +121,6 @@ export function PairLinkModal({
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetServerId && parsedOffer.serverId !== targetServerId) {
|
||||
const message = `That pairing link belongs to ${parsedOffer.serverId}, not ${targetServerId}.`;
|
||||
setErrorMessage(message);
|
||||
if (!isMobile) {
|
||||
Alert.alert("Wrong daemon", message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
setErrorMessage("");
|
||||
@@ -165,7 +149,7 @@ export function PairLinkModal({
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [daemons, handleClose, isMobile, isSaving, onSaved, targetServerId, upsertDaemonFromOfferUrl]);
|
||||
}, [daemons, handleClose, isMobile, isSaving, onSaved, upsertDaemonFromOfferUrl]);
|
||||
|
||||
return (
|
||||
<AdaptiveModalSheet
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import type { LucideIcon } from "lucide-react-native";
|
||||
import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from "@/constants/layout";
|
||||
|
||||
interface SidebarHeaderRowProps {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
isActive?: boolean;
|
||||
testID?: string;
|
||||
nativeID?: string;
|
||||
accessibilityLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-of-sidebar header row: a sidebar-height pressable with an icon + label
|
||||
* and a full-width border separator beneath. Used as the first element of a
|
||||
* sidebar (workspace "Sessions", settings "Back to workspace"). Owns its own
|
||||
* separator line so both sidebars converge on the same edge and padding.
|
||||
*/
|
||||
export function SidebarHeaderRow({
|
||||
icon: Icon,
|
||||
label,
|
||||
onPress,
|
||||
isActive = false,
|
||||
testID,
|
||||
nativeID,
|
||||
accessibilityLabel,
|
||||
}: SidebarHeaderRowProps) {
|
||||
const { theme } = useUnistyles();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
testID={testID}
|
||||
nativeID={nativeID}
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={accessibilityLabel ?? label}
|
||||
style={({ hovered }) => [styles.button, (hovered || isActive) && styles.buttonHovered]}
|
||||
>
|
||||
{({ hovered }) => {
|
||||
const isHighlighted = hovered || isActive;
|
||||
const iconColor = isHighlighted ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
return (
|
||||
<>
|
||||
<Icon size={theme.iconSize.md} color={iconColor} />
|
||||
<Text style={[styles.label, isHighlighted && styles.labelHighlighted]}>{label}</Text>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
height: {
|
||||
xs: HEADER_INNER_HEIGHT_MOBILE,
|
||||
md: HEADER_INNER_HEIGHT,
|
||||
},
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
justifyContent: "center",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
userSelect: "none",
|
||||
},
|
||||
button: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
},
|
||||
buttonHovered: {
|
||||
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||
},
|
||||
label: {
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
labelHighlighted: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,18 @@
|
||||
import { View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
|
||||
/**
|
||||
* Edge-to-edge separator line for sidebars. Must render outside any horizontally
|
||||
* padded container so the line runs the full sidebar width — matching the
|
||||
* separator beneath the workspace sidebar's "New agent" header.
|
||||
*/
|
||||
export function SidebarSeparator() {
|
||||
return <View style={styles.line} />;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
line: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
}));
|
||||
@@ -181,7 +181,7 @@ export function Button({
|
||||
]}
|
||||
>
|
||||
{renderIcon()}
|
||||
<Text style={resolvedTextStyle}>{children}</Text>
|
||||
{children != null ? <Text style={resolvedTextStyle}>{children}</Text> : null}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { Pressable, Text, View, ScrollView } from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { QrCode, Link2, ClipboardPaste, ExternalLink } from "lucide-react-native";
|
||||
import { StyleSheet, useUnistyles, withUnistyles } 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";
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/runtime/host-runtime";
|
||||
import { AddHostModal } from "./add-host-modal";
|
||||
import { PairLinkModal } from "./pair-link-modal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { resolveAppVersion } from "@/utils/app-version";
|
||||
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
|
||||
import { buildHostRootRoute } from "@/utils/host-routes";
|
||||
@@ -20,6 +21,8 @@ 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;
|
||||
@@ -30,6 +33,10 @@ type WelcomeAction = {
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
},
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
@@ -47,14 +54,17 @@ const styles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.xl,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
marginBottom: theme.spacing[3],
|
||||
textAlign: "center",
|
||||
},
|
||||
subtitle: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontSize: theme.fontSize.sm,
|
||||
textAlign: "center",
|
||||
marginBottom: theme.spacing[8],
|
||||
},
|
||||
copyBlock: {
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
marginBottom: theme.spacing[12],
|
||||
},
|
||||
actions: {
|
||||
width: "100%",
|
||||
@@ -117,19 +127,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.destructive,
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
setupHint: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
textAlign: "center",
|
||||
marginBottom: theme.spacing[6],
|
||||
lineHeight: theme.fontSize.sm * 1.5,
|
||||
},
|
||||
setupLink: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
marginBottom: theme.spacing[6],
|
||||
},
|
||||
setupLinkText: {
|
||||
color: theme.colors.accent,
|
||||
@@ -142,6 +144,10 @@ const styles = StyleSheet.create((theme) => ({
|
||||
textAlign: "center",
|
||||
marginTop: theme.spacing[6],
|
||||
},
|
||||
settingsButton: {
|
||||
alignSelf: "center",
|
||||
marginTop: theme.spacing[6],
|
||||
},
|
||||
}));
|
||||
|
||||
function useAnyHostOnline(serverIds: string[]): string | null {
|
||||
@@ -243,13 +249,7 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
|
||||
const anyOnlineServerId = useAnyHostOnline(hosts.map((h) => h.serverId));
|
||||
|
||||
useEffect(() => {
|
||||
const currentPathname = typeof window === "undefined" ? null : window.location.pathname || null;
|
||||
if (currentPathname && currentPathname !== "/welcome") {
|
||||
return;
|
||||
}
|
||||
if (!anyOnlineServerId) {
|
||||
return;
|
||||
}
|
||||
if (!anyOnlineServerId) return;
|
||||
router.replace(buildHostRootRoute(anyOnlineServerId));
|
||||
}, [anyOnlineServerId, router]);
|
||||
|
||||
@@ -309,8 +309,8 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
|
||||
const showHostList = hosts.length > 0 && !anyOnlineServerId;
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{ flex: 1, backgroundColor: theme.colors.surface0 }}
|
||||
<ThemedScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={[
|
||||
styles.container,
|
||||
{ paddingBottom: theme.spacing[6] + insets.bottom },
|
||||
@@ -320,22 +320,25 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<PaseoLogo size={96} />
|
||||
<Text style={styles.title}>Welcome to Paseo</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{showHostList ? "Connecting to your hosts…" : "Connect to your host to start"}
|
||||
</Text>
|
||||
|
||||
{!showHostList && isNative && (
|
||||
<>
|
||||
<Text style={styles.setupHint}>
|
||||
You need the Paseo desktop app or server running on your computer first.
|
||||
</Text>
|
||||
<Pressable style={styles.setupLink} onPress={() => openExternalUrl("https://paseo.sh")}>
|
||||
<Text style={styles.setupLinkText}>Get started at paseo.sh</Text>
|
||||
<ExternalLink size={14} color={theme.colors.accent} />
|
||||
</Pressable>
|
||||
</>
|
||||
)}
|
||||
<View style={styles.copyBlock}>
|
||||
<Text style={styles.title}>Welcome to Paseo</Text>
|
||||
{showHostList ? (
|
||||
<Text style={styles.subtitle}>Connecting to your hosts…</Text>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.subtitle}>Connect your computer to get started</Text>
|
||||
{isNative ? (
|
||||
<Pressable
|
||||
style={styles.setupLink}
|
||||
onPress={() => openExternalUrl("https://paseo.sh")}
|
||||
>
|
||||
<Text style={styles.setupLinkText}>paseo.sh</Text>
|
||||
<ExternalLink size={14} color={theme.colors.accent} />
|
||||
</Pressable>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
{actions.map((action) => {
|
||||
@@ -366,6 +369,17 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={Settings}
|
||||
onPress={() => router.push("/settings")}
|
||||
style={styles.settingsButton}
|
||||
testID="welcome-open-settings"
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
</View>
|
||||
<Text style={styles.versionLabel}>{appVersionText}</Text>
|
||||
|
||||
@@ -386,6 +400,6 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
|
||||
finishOnboarding(serverId);
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
</ThemedScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user