mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): refine Projects navigation and overview hierarchy (#1956)
This commit is contained in:
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user