feat(desktop): refine Projects navigation and overview hierarchy (#1956)

This commit is contained in:
thomaspblock
2026-07-16 18:03:08 +02:00
committed by GitHub
parent ca8324d2cf
commit 158259d953
4 changed files with 76 additions and 152 deletions
@@ -6,7 +6,7 @@ import { TabsList, TabsTrigger } from "@/shared/ui/tabs";
const PROJECT_TAB_TRIGGER_BASE_CLASS =
"rounded-full text-foreground hover:bg-accent hover:text-accent-foreground data-[state=active]:bg-sidebar-active data-[state=active]:text-sidebar-active-foreground data-[state=active]:shadow-xs data-[state=active]:hover:bg-sidebar-active data-[state=active]:hover:text-sidebar-active-foreground";
const PROJECT_TAB_TRIGGER_CLASS = `${PROJECT_TAB_TRIGGER_BASE_CLASS} h-7 gap-1.5 px-2 text-xs xl:px-2.5 xl:text-sm`;
const PROJECT_TAB_TRIGGER_CLASS = `${PROJECT_TAB_TRIGGER_BASE_CLASS} h-8 gap-1.5 px-3 text-xs xl:px-3.5 xl:text-sm`;
const PROJECT_ICON_TAB_TRIGGER_CLASS = `${PROJECT_TAB_TRIGGER_BASE_CLASS} h-8 w-8 shrink-0 border border-border/60 p-2 data-[state=active]:border-transparent`;
@@ -79,7 +79,7 @@ function StatPill({
</span>
<Icon className="h-3.5 w-3.5 text-muted-foreground/70" />
</span>
<span className="mt-4 text-3xl font-semibold leading-none tracking-tight text-foreground">
<span className="mt-auto pt-4 text-4xl font-semibold leading-none tracking-tight text-foreground">
{count}
</span>
</button>
@@ -1,4 +1,4 @@
import { LayoutGrid, List, Search } from "lucide-react";
import { LayoutGrid, List } from "lucide-react";
import type {
ProjectsFilter,
@@ -13,13 +13,6 @@ const SELECTED_MENU_ITEM_CLASSES =
type ProjectsToolbarProps = {
filter: ProjectsFilter;
onFilterChange: (filter: ProjectsFilter) => void;
searchOpen: boolean;
onSearchOpenChange: (open: boolean) => void;
/**
* When true the leading search button is pulled into the activity-timeline
* gutter so it lines up with the timeline node icons below it.
*/
timeline?: boolean;
};
export function ProjectsViewModeToggle({
@@ -61,9 +54,6 @@ export function ProjectsViewModeToggle({
export function ProjectsToolbar({
filter,
onFilterChange,
searchOpen,
onSearchOpenChange,
timeline = false,
}: ProjectsToolbarProps) {
const filterOptions: Array<{
compactLabel?: string;
@@ -78,35 +68,16 @@ export function ProjectsToolbar({
},
{ compactLabel: "PRs", label: "Pull Requests", value: "prs" },
{ label: "Issues", value: "issues" },
{ label: "Mine", value: "mine" },
{ compactLabel: "Mine", label: "My Repositories", value: "mine" },
{ label: "Local", value: "local" },
];
return (
<div
className={cn(
"pointer-events-auto flex min-h-[3.25rem] min-w-0 items-center py-2",
timeline ? "pl-2 pr-4" : "px-4",
)}
className="pointer-events-auto flex min-h-[3.25rem] min-w-0 items-center px-4 py-2"
data-tauri-drag-region
>
<div className="flex min-w-0 flex-1 items-center gap-0.5 overflow-hidden">
<Button
aria-expanded={searchOpen}
aria-label="Ask an agent about your projects"
className={cn(
"h-8 w-8 shrink-0 rounded-full px-0",
searchOpen
? SELECTED_MENU_ITEM_CLASSES
: "border border-border/60 bg-transparent",
)}
onClick={() => onSearchOpenChange(!searchOpen)}
size="sm"
type="button"
variant="ghost"
>
<Search className="h-4 w-4" />
</Button>
<fieldset className="flex min-w-0 flex-1 flex-nowrap items-center gap-1 overflow-x-auto scrollbar-none [&::-webkit-scrollbar]:hidden">
<legend className="sr-only">Project owner filter</legend>
{filterOptions.map((option) => (
@@ -114,10 +85,8 @@ export function ProjectsToolbar({
aria-label={option.label}
aria-pressed={filter === option.value}
className={cn(
"h-7 shrink-0 gap-1.5 rounded-full px-2 text-xs xl:px-2.5 xl:text-sm",
!searchOpen &&
filter === option.value &&
SELECTED_MENU_ITEM_CLASSES,
"h-8 shrink-0 gap-1.5 rounded-full px-3 text-xs xl:px-3.5 xl:text-sm",
filter === option.value && SELECTED_MENU_ITEM_CLASSES,
)}
key={option.value}
onClick={() => onFilterChange(option.value)}
+69 -114
View File
@@ -30,7 +30,6 @@ import { ProjectsIssuesList } from "@/features/projects/ui/ProjectsIssuesList";
import { ProjectsOverviewPanel } from "@/features/projects/ui/ProjectsOverviewPanel";
import { ProjectsOverviewRail } from "@/features/projects/ui/ProjectsOverviewRail";
import { ProjectsPullRequestsList } from "@/features/projects/ui/ProjectsPullRequestsList";
import { ProjectsAgentPromptPage } from "@/features/projects/ui/ProjectsAgentPromptPage";
import {
ProjectsToolbar,
ProjectsViewModeToggle,
@@ -58,13 +57,7 @@ import { useOpenProjectTerminal } from "@/features/projects/ui/useOpenProjectTer
import { useCommunities } from "@/features/communities/useCommunities";
import { CommunityEmojiIcon } from "@/features/communities/ui/CommunitySwitcher";
import { useIdentityQuery } from "@/shared/api/hooks";
import { useMainInsetRef } from "@/shared/layout/MainInsetContext";
import {
channelChrome,
channelContentTopPaddingMeasurement,
topChromeInset,
} from "@/shared/layout/chromeLayout";
import { useMeasuredCssVariable } from "@/shared/layout/useMeasuredCssVariable";
import { topChromeInset } from "@/shared/layout/chromeLayout";
import { cn } from "@/shared/lib/cn";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { Button } from "@/shared/ui/button";
@@ -113,11 +106,6 @@ export function ProjectsView() {
},
[],
);
const mainInsetRef = useMainInsetRef();
const projectsHeaderChromeRef = useMeasuredCssVariable({
targetRef: mainInsetRef,
...channelContentTopPaddingMeasurement,
});
const projectsQuery = useProjectsQuery();
const identityQuery = useIdentityQuery();
const projects = projectsQuery.data ?? [];
@@ -142,7 +130,6 @@ export function ProjectsView() {
snapshotProjects,
activeCommunity?.reposDir,
);
const [searchOpen, setSearchOpen] = React.useState(false);
const [createProjectOpen, setCreateProjectOpen] = React.useState(false);
const createProjectMutation = useCreateProjectMutation();
const [storedViewMode, setStoredViewMode] =
@@ -203,8 +190,6 @@ export function ProjectsView() {
const handleFilterChange = React.useCallback((nextFilter: ProjectsFilter) => {
setFilter(nextFilter);
writeStoredFilter(nextFilter);
// Picking a tab exits the full-page search state.
setSearchOpen(false);
}, []);
const handleSortChange = React.useCallback((nextSort: ProjectsSort) => {
@@ -527,14 +512,8 @@ export function ProjectsView() {
</div>
);
const renderProjectsToolbar = (timeline: boolean) => (
<ProjectsToolbar
filter={filter}
onFilterChange={handleFilterChange}
onSearchOpenChange={setSearchOpen}
searchOpen={searchOpen}
timeline={timeline}
/>
const renderProjectsToolbar = () => (
<ProjectsToolbar filter={filter} onFilterChange={handleFilterChange} />
);
return (
@@ -580,99 +559,75 @@ export function ProjectsView() {
onOpenChange={setCreateProjectOpen}
open={createProjectOpen}
/>
{searchOpen ? (
<>
<div
className={cn(
"pointer-events-none relative z-30 overflow-hidden bg-background/80 backdrop-blur-md supports-backdrop-filter:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/55",
channelChrome.negativeMargin,
)}
ref={projectsHeaderChromeRef}
>
{projectsHeader}
{renderProjectsToolbar(false)}
<div
className="buzz-content-scrollbar flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden overflow-y-auto"
onScroll={handleContentScroll}
>
{projectsHeader}
{filter === "all" ? null : (
<div className="sticky top-0 z-30 bg-background/80 backdrop-blur-md supports-backdrop-filter:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/55">
{renderProjectsToolbar()}
</div>
<ProjectsAgentPromptPage
onClose={() => setSearchOpen(false)}
projects={projects}
workspaceId={activeCommunity?.id ?? null}
/>
</>
) : (
<div
className="buzz-content-scrollbar flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden overflow-y-auto"
onScroll={handleContentScroll}
>
{projectsHeader}
{filter === "all" ? null : (
<div className="sticky top-0 z-30 bg-background/80 backdrop-blur-md supports-backdrop-filter:bg-background/70 dark:bg-background/70 dark:backdrop-blur-xl dark:supports-backdrop-filter:bg-background/55">
{renderProjectsToolbar(false)}
</div>
)}
<div className="w-full min-w-0 px-4 pb-4 pt-4">
{filter === "all" ? (
<ProjectsOverviewPanel
localRepositoryCount={localProjectCount}
metadata={
<ProjectsOverviewRail
profiles={profiles}
projects={projects}
snapshots={repoSnapshotsQuery.data}
snapshotsLoading={repoSnapshotsQuery.isLoading}
summaries={activitySummariesQuery.data}
>
{mostActiveRepositoryItems}
</ProjectsOverviewRail>
}
onSelectSection={handleFilterChange}
projects={projects}
summaries={activitySummariesQuery.data}
>
<div className="sticky top-0 z-30 -mx-4 mb-4 mt-2 bg-card/80 backdrop-blur-md supports-backdrop-filter:bg-card/70">
{renderProjectsToolbar()}
</div>
<section className="space-y-3">{activityFeed}</section>
</ProjectsOverviewPanel>
) : (
<section className="space-y-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h3 className="text-base font-semibold text-foreground">
{filter === "prs"
? "Pull requests"
: filter === "issues"
? "Issues"
: "Repositories"}
</h3>
{listControls}
</div>
{filter === "prs" ? (
<ProjectsPullRequestsList
isLoading={projectPullRequestsQuery.isLoading}
onOpen={handleOpenPullRequest}
profiles={profiles}
pullRequests={visiblePullRequests}
viewMode={viewMode}
/>
) : filter === "issues" ? (
<ProjectsIssuesList
isLoading={projectIssuesQuery.isLoading}
issues={visibleIssues}
onOpen={handleOpenIssue}
profiles={profiles}
viewMode={viewMode}
/>
) : (
repositoryItems
)}
</section>
)}
<div className="w-full min-w-0 px-4 pb-4 pt-4">
{filter === "all" ? (
<ProjectsOverviewPanel
localRepositoryCount={localProjectCount}
metadata={
<ProjectsOverviewRail
profiles={profiles}
projects={projects}
snapshots={repoSnapshotsQuery.data}
snapshotsLoading={repoSnapshotsQuery.isLoading}
summaries={activitySummariesQuery.data}
>
{mostActiveRepositoryItems}
</ProjectsOverviewRail>
}
onSelectSection={handleFilterChange}
projects={projects}
summaries={activitySummariesQuery.data}
>
<div className="sticky top-0 z-30 -mx-4 mb-4 mt-2 bg-card/80 backdrop-blur-md supports-backdrop-filter:bg-card/70">
<div
aria-hidden="true"
className="pointer-events-none absolute -bottom-6 left-[23px] top-[2.625rem] z-0 w-px bg-border/45"
/>
{renderProjectsToolbar(true)}
</div>
<section className="space-y-3">{activityFeed}</section>
</ProjectsOverviewPanel>
) : (
<section className="space-y-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h3 className="text-base font-semibold text-foreground">
{filter === "prs"
? "Pull requests"
: filter === "issues"
? "Issues"
: "Repositories"}
</h3>
{listControls}
</div>
{filter === "prs" ? (
<ProjectsPullRequestsList
isLoading={projectPullRequestsQuery.isLoading}
onOpen={handleOpenPullRequest}
profiles={profiles}
pullRequests={visiblePullRequests}
viewMode={viewMode}
/>
) : filter === "issues" ? (
<ProjectsIssuesList
isLoading={projectIssuesQuery.isLoading}
issues={visibleIssues}
onOpen={handleOpenIssue}
profiles={profiles}
viewMode={viewMode}
/>
) : (
repositoryItems
)}
</section>
)}
</div>
</div>
)}
</div>
</div>
);
}