feat(sidebar): make empty project actions clear and quiet (#5675)

Use the shared button sizes and a bordered list card so the empty sidebar gives project setup actions a consistent visual hierarchy.
This commit is contained in:
Mohamed Boudra
2026-09-29 12:02:38 +01:00
committed by GitHub
parent be2fb1cab9
commit d596861891
7 changed files with 53 additions and 25 deletions
+1 -1
View File
@@ -193,7 +193,7 @@ Terminology:
Loading is inline by default. `<LoadingSpinner size={14} color={foregroundMuted} />` sits next to the thing it relates to (`packages/app/src/screens/settings/providers-section.tsx:227-231`). Page-level loading is a centered `<LoadingSpinner size="large">` (`packages/app/src/screens/sessions-screen.tsx:69-72`). Card-level loading is a single short line, not a spinner. In-row dropdown items use `<DropdownMenuItem status="pending" pendingLabel="Removing...">`; the menu item handles its own pending state.
Empty states are short noun phrases. Centered, muted, one or two lines. Sessions screen pairs the empty noun with a single ghost button to navigate back (`packages/app/src/screens/sessions-screen.tsx:74-81`); that pairing is the maximum elaboration. Illustrations and CTAs disguised as empty states are wrong.
Screen-level empty states use a centered, muted noun phrase and at most one ghost button (`packages/app/src/screens/sessions-screen.tsx`). Sidebar list empty states use a quiet bordered card with a left-aligned `sm` title, a muted `sm` description, and `xs` `<Button>` actions. Use `secondary` for the primary action and `outline` for a secondary action (`packages/app/src/components/sidebar/empty-states.tsx`). Keep both forms short and free of illustrations.
Inline errors are a single sentence in `palette.red[300]` `xs`, sitting under the field or inside the card it relates to (`packages/app/src/screens/settings/providers-section.tsx:115-119`).
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

@@ -1,4 +1,4 @@
import { useCallback } from "react";
import { type ReactNode, useCallback } from "react";
import { Import, Plus } from "lucide-react-native";
import { useTranslation } from "react-i18next";
import { Text, View } from "react-native";
@@ -6,6 +6,28 @@ import { StyleSheet } from "react-native-unistyles";
import { Button } from "@/components/ui/button";
import { useSidebarViewStore } from "@/stores/sidebar-view-store";
function SidebarEmptyStateCard({
testID,
title,
description,
children,
}: {
testID: string;
title: string;
description: string;
children: ReactNode;
}) {
return (
<View style={styles.card} testID={testID}>
<View style={styles.copy}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.description}>{description}</Text>
</View>
<View style={styles.actions}>{children}</View>
</View>
);
}
/**
* The two things the sidebar's workspace list says when it has no rows.
*
@@ -27,13 +49,15 @@ export function SidebarFilterEmptyState() {
}, [clearLabelFilter, clearProjectFilters]);
return (
<View style={styles.container} testID="sidebar-filter-empty-state">
<Text style={styles.title}>{t("sidebar.filterEmpty.title")}</Text>
<Text style={styles.description}>{t("sidebar.filterEmpty.description")}</Text>
<Button variant="ghost" size="sm" onPress={clearFilters}>
<SidebarEmptyStateCard
testID="sidebar-filter-empty-state"
title={t("sidebar.filterEmpty.title")}
description={t("sidebar.filterEmpty.description")}
>
<Button variant="secondary" size="xs" onPress={clearFilters}>
{t("sidebar.filterEmpty.clear")}
</Button>
</View>
</SidebarEmptyStateCard>
);
}
@@ -47,40 +71,44 @@ export function SidebarProjectEmptyState({
const { t } = useTranslation();
return (
<View style={styles.container} testID="sidebar-project-empty-state">
<Text style={styles.title}>{t("sidebar.project.empty.title")}</Text>
<Text style={styles.description}>{t("sidebar.project.empty.description")}</Text>
<Button variant="ghost" size="sm" leftIcon={Plus} onPress={onAddProject}>
<SidebarEmptyStateCard
testID="sidebar-project-empty-state"
title={t("sidebar.project.empty.title")}
description={t("sidebar.project.empty.description")}
>
<Button variant="secondary" size="xs" leftIcon={Plus} onPress={onAddProject}>
{t("sidebar.actions.addProject")}
</Button>
<Button variant="ghost" size="sm" leftIcon={Import} onPress={onImportSession}>
<Button variant="outline" size="xs" leftIcon={Import} onPress={onImportSession}>
{t("importSession.title")}
</Button>
</View>
</SidebarEmptyStateCard>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
marginHorizontal: theme.spacing[2],
marginTop: theme.spacing[4],
paddingTop: theme.spacing[6],
paddingBottom: theme.spacing[4],
paddingHorizontal: theme.spacing[4],
card: {
marginTop: theme.spacing[3],
padding: theme.spacing[4],
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.lg,
backgroundColor: theme.colors.surface0,
alignItems: "center",
gap: theme.spacing[3],
},
copy: {
gap: theme.spacing[1],
},
actions: {
flexDirection: "row",
gap: theme.spacing[2],
marginTop: theme.spacing[4],
},
title: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium,
textAlign: "center",
fontWeight: theme.fontWeight.normal,
},
description: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
textAlign: "center",
},
}));