feat(app): add empty states for sidebar, sessions, and open-project
Sidebar shows a card with "No projects yet" and an Add project ghost button. Sessions screen shows "No sessions yet" with a ghost Back button. Open-project screen uses MenuHeader borderless, shared Button, and shows introductory text when no projects exist.
This commit is contained in:
@@ -520,6 +520,7 @@ function MobileSidebar({
|
|||||||
isRefreshing={isManualRefresh && isRevalidating}
|
isRefreshing={isManualRefresh && isRevalidating}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
onWorkspacePress={closeToAgent}
|
onWorkspacePress={closeToAgent}
|
||||||
|
onAddProject={handleOpenProject}
|
||||||
parentGestureRef={closeGestureRef}
|
parentGestureRef={closeGestureRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -701,6 +702,7 @@ function DesktopSidebar({
|
|||||||
projects={projects}
|
projects={projects}
|
||||||
isRefreshing={isManualRefresh && isRevalidating}
|
isRefreshing={isManualRefresh && isRevalidating}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
onAddProject={handleOpenProject}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
Monitor,
|
Monitor,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
|
Plus,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import { NestableScrollContainer } from "react-native-draggable-flatlist";
|
import { NestableScrollContainer } from "react-native-draggable-flatlist";
|
||||||
import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list";
|
import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list";
|
||||||
@@ -73,6 +74,7 @@ import { confirmDialog } from "@/utils/confirm-dialog";
|
|||||||
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
||||||
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
import { shouldRenderSyncedStatusLoader } from "@/utils/status-loader";
|
||||||
import { getStatusDotColor } from "@/utils/status-dot-color";
|
import { getStatusDotColor } from "@/utils/status-dot-color";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { Shortcut } from "@/components/ui/shortcut";
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
import type { ShortcutKey } from "@/utils/format-shortcut";
|
import type { ShortcutKey } from "@/utils/format-shortcut";
|
||||||
@@ -113,6 +115,7 @@ interface SidebarWorkspaceListProps {
|
|||||||
isRefreshing?: boolean;
|
isRefreshing?: boolean;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onWorkspacePress?: () => void;
|
onWorkspacePress?: () => void;
|
||||||
|
onAddProject?: () => void;
|
||||||
listFooterComponent?: ReactElement | null;
|
listFooterComponent?: ReactElement | null;
|
||||||
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
|
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
|
||||||
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||||
@@ -1631,6 +1634,7 @@ export function SidebarWorkspaceList({
|
|||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
|
onAddProject,
|
||||||
listFooterComponent,
|
listFooterComponent,
|
||||||
parentGestureRef,
|
parentGestureRef,
|
||||||
}: SidebarWorkspaceListProps) {
|
}: SidebarWorkspaceListProps) {
|
||||||
@@ -1900,7 +1904,18 @@ export function SidebarWorkspaceList({
|
|||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
{projects.length === 0 ? (
|
{projects.length === 0 ? (
|
||||||
<Text style={styles.emptyText}>No projects yet</Text>
|
<View style={styles.emptyContainer}>
|
||||||
|
<Text style={styles.emptyTitle}>No projects yet</Text>
|
||||||
|
<Text style={styles.emptyText}>Add a project to get started</Text>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
leftIcon={Plus}
|
||||||
|
onPress={onAddProject}
|
||||||
|
>
|
||||||
|
Add project
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<DraggableList
|
<DraggableList
|
||||||
testID="sidebar-project-list"
|
testID="sidebar-project-list"
|
||||||
@@ -1963,11 +1978,27 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
marginBottom: theme.spacing[1],
|
marginBottom: theme.spacing[1],
|
||||||
},
|
},
|
||||||
workspaceListContainer: {},
|
workspaceListContainer: {},
|
||||||
|
emptyContainer: {
|
||||||
|
marginHorizontal: theme.spacing[2],
|
||||||
|
marginTop: theme.spacing[4],
|
||||||
|
paddingTop: theme.spacing[6],
|
||||||
|
paddingBottom: theme.spacing[4],
|
||||||
|
paddingHorizontal: theme.spacing[4],
|
||||||
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
backgroundColor: theme.colors.surface0,
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[3],
|
||||||
|
},
|
||||||
|
emptyTitle: {
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: theme.fontWeight.medium,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
emptyText: {
|
emptyText: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
marginTop: theme.spacing[8],
|
|
||||||
marginHorizontal: theme.spacing[2],
|
|
||||||
},
|
},
|
||||||
projectRow: {
|
projectRow: {
|
||||||
minHeight: 36,
|
minHeight: 36,
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { View, Text, Pressable } from "react-native";
|
import { View, Text } from "react-native";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import { FolderOpen } from "lucide-react-native";
|
import { FolderOpen } from "lucide-react-native";
|
||||||
import { PaseoLogo } from "@/components/icons/paseo-logo";
|
import { PaseoLogo } from "@/components/icons/paseo-logo";
|
||||||
import { SidebarMenuToggle } from "@/components/headers/menu-header";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { MenuHeader } from "@/components/headers/menu-header";
|
||||||
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { useDesktopDragHandlers, useTrafficLightPadding } from "@/utils/desktop-window";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
|
import { useDesktopDragHandlers } from "@/utils/desktop-window";
|
||||||
|
|
||||||
export function OpenProjectScreen({ serverId }: { serverId: string }) {
|
export function OpenProjectScreen({ serverId }: { serverId: string }) {
|
||||||
const { theme } = useUnistyles();
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
const trafficLightPadding = useTrafficLightPadding();
|
|
||||||
const desktopAgentListOpen = usePanelStore((s) => s.desktop.agentListOpen);
|
|
||||||
const openAgentList = usePanelStore((s) => s.openAgentList);
|
const openAgentList = usePanelStore((s) => s.openAgentList);
|
||||||
const openProjectPicker = useOpenProjectPicker(serverId);
|
const openProjectPicker = useOpenProjectPicker(serverId);
|
||||||
|
const hasHydrated = useSessionStore((s) => s.sessions[serverId]?.hasHydratedWorkspaces ?? false);
|
||||||
|
const hasProjects = useSessionStore((s) => (s.sessions[serverId]?.workspaces?.size ?? 0) > 0);
|
||||||
|
|
||||||
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
const collapsedSidebarInset =
|
|
||||||
!isMobile && !desktopAgentListOpen && trafficLightPadding.side
|
|
||||||
? trafficLightPadding
|
|
||||||
: { left: 0, right: 0 };
|
|
||||||
const dragHandlers = useDesktopDragHandlers();
|
const dragHandlers = useDesktopDragHandlers();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -32,22 +27,24 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container} {...dragHandlers}>
|
<View style={styles.container} {...dragHandlers}>
|
||||||
<View style={[styles.menuToggle, { paddingTop: insets.top, paddingLeft: collapsedSidebarInset.left, paddingRight: collapsedSidebarInset.right }]}>
|
<MenuHeader borderless />
|
||||||
<SidebarMenuToggle />
|
|
||||||
</View>
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<PaseoLogo size={56} />
|
<View style={styles.logo}>
|
||||||
<Text style={styles.heading}>What shall we build today?</Text>
|
<PaseoLogo size={56} />
|
||||||
<Pressable
|
</View>
|
||||||
style={({ hovered }) => [styles.openButton, hovered && styles.openButtonHovered]}
|
<View style={styles.headingGroup}>
|
||||||
onPress={() => {
|
<Text style={styles.heading}>What shall we build today?</Text>
|
||||||
void openProjectPicker();
|
{hasHydrated && !hasProjects ? (
|
||||||
}}
|
<Text style={styles.subtitle}>
|
||||||
testID="open-project-submit"
|
Add a project folder to start running agents on your codebase
|
||||||
>
|
</Text>
|
||||||
<FolderOpen size={16} color={theme.colors.foregroundMuted} />
|
) : null}
|
||||||
<Text style={styles.openButtonText}>Add a project</Text>
|
</View>
|
||||||
</Pressable>
|
<View style={styles.cta}>
|
||||||
|
<Button variant="default" leftIcon={FolderOpen} onPress={() => void openProjectPicker()} testID="open-project-submit">
|
||||||
|
Add a project
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -59,43 +56,32 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface0,
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
},
|
},
|
||||||
menuToggle: {
|
|
||||||
position: "absolute",
|
|
||||||
top: theme.spacing[3],
|
|
||||||
left: theme.spacing[3],
|
|
||||||
zIndex: 1,
|
|
||||||
},
|
|
||||||
content: {
|
content: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[6],
|
gap: 0,
|
||||||
padding: theme.spacing[6],
|
padding: theme.spacing[6],
|
||||||
},
|
},
|
||||||
|
logo: {
|
||||||
|
marginBottom: theme.spacing[8],
|
||||||
|
},
|
||||||
|
headingGroup: {
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[3],
|
||||||
|
},
|
||||||
|
cta: {
|
||||||
|
marginTop: theme.spacing[12],
|
||||||
|
},
|
||||||
heading: {
|
heading: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
fontSize: theme.fontSize["2xl"],
|
fontSize: theme.fontSize["2xl"],
|
||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
openButton: {
|
subtitle: {
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
paddingVertical: theme.spacing[3],
|
|
||||||
paddingHorizontal: theme.spacing[4],
|
|
||||||
borderRadius: theme.borderRadius.lg,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
},
|
|
||||||
openButtonHovered: {
|
|
||||||
borderColor: theme.colors.borderAccent,
|
|
||||||
backgroundColor: theme.colors.surface1,
|
|
||||||
},
|
|
||||||
openButtonText: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: theme.fontWeight.normal,
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useMemo, useState, useCallback, useEffect } from "react";
|
import { useMemo, useState, useCallback, useEffect } from "react";
|
||||||
import { View } from "react-native";
|
import { View, Text } from "react-native";
|
||||||
import { useIsFocused } from "@react-navigation/native";
|
import { useIsFocused } from "@react-navigation/native";
|
||||||
|
import { router } from "expo-router";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
|
import { ChevronLeft } from "lucide-react-native";
|
||||||
import { MenuHeader } from "@/components/headers/menu-header";
|
import { MenuHeader } from "@/components/headers/menu-header";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { AgentList } from "@/components/agent-list";
|
import { AgentList } from "@/components/agent-list";
|
||||||
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
|
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
|
||||||
|
import { buildHostOpenProjectRoute } from "@/utils/host-routes";
|
||||||
|
|
||||||
export function SessionsScreen({ serverId }: { serverId: string }) {
|
export function SessionsScreen({ serverId }: { serverId: string }) {
|
||||||
const isFocused = useIsFocused();
|
const isFocused = useIsFocused();
|
||||||
@@ -44,13 +48,26 @@ function SessionsScreenContent({ serverId }: { serverId: string }) {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<MenuHeader title="Sessions" />
|
<MenuHeader title="Sessions" />
|
||||||
<AgentList
|
{sortedAgents.length === 0 ? (
|
||||||
agents={sortedAgents}
|
<View style={styles.emptyContainer}>
|
||||||
showCheckoutInfo={false}
|
<Text style={styles.emptyText}>No sessions yet</Text>
|
||||||
isRefreshing={isManualRefresh && isRevalidating}
|
<Button
|
||||||
onRefresh={handleRefresh}
|
variant="ghost"
|
||||||
showAttentionIndicator={false}
|
leftIcon={ChevronLeft}
|
||||||
/>
|
onPress={() => router.navigate(buildHostOpenProjectRoute(serverId) as any)}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<AgentList
|
||||||
|
agents={sortedAgents}
|
||||||
|
showCheckoutInfo={false}
|
||||||
|
isRefreshing={isManualRefresh && isRevalidating}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
showAttentionIndicator={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -60,4 +77,15 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface0,
|
||||||
},
|
},
|
||||||
|
emptyContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[6],
|
||||||
|
padding: theme.spacing[6],
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.lg,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user