fix: ui polish — unified topbar, selector consistency, quick actions editor improvements (#693)

This commit is contained in:
Carlos Florêncio
2026-04-24 21:31:46 +01:00
committed by GitHub
parent 1fc3544625
commit c092fdeb1c
22 changed files with 628 additions and 274 deletions
+52 -32
View File
@@ -2,8 +2,9 @@
import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
import { IconArrowLeft, IconBrandGithub } from "@tabler/icons-react";
import { IconBrandGithub } from "@tabler/icons-react";
import { Alert, AlertDescription } from "@kandev/ui/alert";
import { PageTopbar } from "@/components/page-topbar";
import { useGitHubStatus } from "@/hooks/domains/github/use-github-status";
import type { Repository, Workflow, WorkflowStep } from "@/lib/types/http";
import type { GitHubIssue, GitHubPR } from "@/lib/types/github";
@@ -13,8 +14,16 @@ import {
PresetsSidebar,
type SidebarSelection,
} from "@/components/github/my-github/presets-sidebar";
import { PR_PRESETS, ISSUE_PRESETS } from "@/components/github/my-github/search-bar";
import {
PR_PRESETS,
ISSUE_PRESETS,
type PresetOption,
} from "@/components/github/my-github/search-bar";
import { useGitHubSearch } from "@/components/github/my-github/use-github-search";
import {
useDefaultQueryPresets,
resolvePresetOptions,
} from "@/components/github/my-github/use-default-query-presets";
import { useSavedPresets, type SavedPreset } from "@/components/github/my-github/use-saved-presets";
import { useKnownRepos, resetKnownReposStore } from "@/components/github/my-github/use-known-repos";
import { useCommittedQuery } from "@/components/github/my-github/use-committed-query";
@@ -41,16 +50,11 @@ type GitHubPageClientProps = {
function PageHeader() {
return (
<header className="flex items-center gap-3 px-4 py-3 border-b shrink-0">
<Link href="/" className="text-muted-foreground hover:text-foreground cursor-pointer">
<IconArrowLeft className="h-4 w-4" />
</Link>
<IconBrandGithub className="h-5 w-5" />
<h1 className="text-lg font-semibold">My GitHub</h1>
<span className="text-xs text-muted-foreground ml-2">
Pull requests and issues across your repos.
</span>
</header>
<PageTopbar
title="GitHub"
subtitle="Pull requests and issues across your repos."
icon={<IconBrandGithub className="h-4 w-4" />}
/>
);
}
@@ -81,11 +85,16 @@ function NoWorkspaceNotice() {
);
}
function resolveTitle(selection: SidebarSelection, saved: SavedPreset[]): string {
function resolveTitle(
selection: SidebarSelection,
saved: SavedPreset[],
prPresets: PresetOption[],
issuePresets: PresetOption[],
): string {
if (selection.source === "saved") {
return saved.find((p) => p.id === selection.id)?.label ?? "Saved query";
}
const presets = selection.kind === "pr" ? PR_PRESETS : ISSUE_PRESETS;
const presets = selection.kind === "pr" ? prPresets : issuePresets;
return (
presets.find((p) => p.value === selection.id)?.label ??
(selection.kind === "pr" ? "Pull requests" : "Issues")
@@ -131,16 +140,6 @@ function ResultsList({
);
}
function defaultSelection(kind: "pr" | "issue"): SidebarSelection {
const presets = kind === "pr" ? PR_PRESETS : ISSUE_PRESETS;
return { kind, source: "preset", id: presets[0]?.value ?? "" };
}
function defaultQuery(kind: "pr" | "issue"): string {
const presets = kind === "pr" ? PR_PRESETS : ISSUE_PRESETS;
return presets[0]?.filter ?? "";
}
type GitHubPageState = ReturnType<typeof useGitHubPageState>;
function useRepoOptions(
@@ -168,15 +167,28 @@ function useRepoOptions(
}, [knownRepos, repoFilter]);
}
function useResolvedQueryPresets() {
const { prPresets: storedPr, issuePresets: storedIssue } = useDefaultQueryPresets();
const pr = useMemo(() => resolvePresetOptions(storedPr, PR_PRESETS), [storedPr]);
const issue = useMemo(() => resolvePresetOptions(storedIssue, ISSUE_PRESETS), [storedIssue]);
return { pr, issue };
}
function useGitHubPageState() {
const [selection, setSelection] = useState<SidebarSelection>(() => defaultSelection("pr"));
const { pr: resolvedPrPresets, issue: resolvedIssuePresets } = useResolvedQueryPresets();
const [selection, setSelection] = useState<SidebarSelection>(() => ({
kind: "pr",
source: "preset",
id: resolvedPrPresets[0]?.value ?? "",
}));
const {
draft: customQuery,
committed: committedQuery,
setDraft: setCustomQuery,
setImmediate: setQueryImmediate,
commit: commitCustomQuery,
} = useCommittedQuery(defaultQuery("pr"));
} = useCommittedQuery(resolvedPrPresets[0]?.filter ?? "");
const [repoFilter, setRepoFilter] = useState("");
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
const {
@@ -185,7 +197,7 @@ function useGitHubPageState() {
remove: removeSavedPreset,
} = useSavedPresets();
const presets = selection.kind === "pr" ? PR_PRESETS : ISSUE_PRESETS;
const presets = selection.kind === "pr" ? resolvedPrPresets : resolvedIssuePresets;
const search = useGitHubSearch<GitHubPR | GitHubIssue>(
selection.kind,
presets,
@@ -194,7 +206,10 @@ function useGitHubPageState() {
repoFilter,
);
const repoOptions = useRepoOptions(selection, committedQuery, search.items, repoFilter);
const title = useMemo(() => resolveTitle(selection, savedPresets), [selection, savedPresets]);
const title = useMemo(
() => resolveTitle(selection, savedPresets, resolvedPrPresets, resolvedIssuePresets),
[selection, savedPresets, resolvedPrPresets, resolvedIssuePresets],
);
const onSelect = useCallback(
(s: SidebarSelection) => {
@@ -205,12 +220,12 @@ function useGitHubPageState() {
setRepoFilter(found?.repoFilter ?? "");
return;
}
const presetList = s.kind === "pr" ? PR_PRESETS : ISSUE_PRESETS;
const presetList = s.kind === "pr" ? resolvedPrPresets : resolvedIssuePresets;
const preset = presetList.find((p) => p.value === s.id);
setQueryImmediate(preset?.filter ?? "");
setRepoFilter("");
},
[savedPresets, setQueryImmediate],
[savedPresets, setQueryImmediate, resolvedPrPresets, resolvedIssuePresets],
);
const canSaveCurrent = customQuery.trim().length > 0 || repoFilter.length > 0;
@@ -225,8 +240,9 @@ function useGitHubPageState() {
const onDeleteSaved = (id: string) => {
removeSavedPreset(id);
if (selection.source === "saved" && selection.id === id) {
setSelection(defaultSelection(selection.kind));
setQueryImmediate(defaultQuery(selection.kind));
const fallbackPresets = selection.kind === "pr" ? resolvedPrPresets : resolvedIssuePresets;
setSelection({ kind: selection.kind, source: "preset", id: fallbackPresets[0]?.value ?? "" });
setQueryImmediate(fallbackPresets[0]?.filter ?? "");
setRepoFilter("");
}
};
@@ -251,6 +267,8 @@ function useGitHubPageState() {
onOpenSaveDialog,
onConfirmSave,
onDeleteSaved,
resolvedPrPresets,
resolvedIssuePresets,
};
}
@@ -278,6 +296,8 @@ function AuthenticatedLayout({
onDeleteSaved={state.onDeleteSaved}
canSaveCurrent={state.canSaveCurrent}
onSaveCurrent={state.onOpenSaveDialog}
prPresets={state.resolvedPrPresets}
issuePresets={state.resolvedIssuePresets}
/>
</aside>
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
+39 -66
View File
@@ -1,9 +1,8 @@
"use client";
import Link from "next/link";
import { IconArrowLeft } from "@tabler/icons-react";
import { Card, CardContent, CardHeader, CardTitle } from "@kandev/ui/card";
import { Button } from "@kandev/ui/button";
import { PageTopbar } from "@/components/page-topbar";
import { ToggleGroup, ToggleGroupItem } from "@kandev/ui/toggle-group";
import type { StatsResponse } from "@/lib/types/http";
import { useMemo, useState } from "react";
@@ -81,13 +80,7 @@ function getRangeLabel(range: RangeKey): string {
function StatsEmptyState({ message }: { message: string }) {
return (
<div className="h-screen w-full flex flex-col bg-background">
<header className="flex items-center gap-3 p-4 pb-3">
<Link href="/" className="text-2xl font-bold hover:opacity-80 cursor-pointer">
KanDev
</Link>
<span className="text-muted-foreground">/</span>
<span className="text-muted-foreground">Statistics</span>
</header>
<PageTopbar title="Statistics" />
<div className="flex-1 flex items-center justify-center">
<p className="text-muted-foreground">{message}</p>
</div>
@@ -105,56 +98,42 @@ type StatsHeaderProps = {
function StatsHeader({ global, range, copied, onRangeChange, onCopy }: StatsHeaderProps) {
return (
<header className="flex items-center gap-3 p-4 pb-3 shrink-0">
<Button variant="ghost" size="sm" asChild className="cursor-pointer">
<Link href="/">
<IconArrowLeft className="h-4 w-4" />
Back
</Link>
</Button>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Link href="/" className="font-semibold text-foreground hover:opacity-80 cursor-pointer">
KanDev
</Link>
<span>›</span>
<span>Statistics</span>
<span className="text-muted-foreground/60">·</span>
<span className="font-mono text-xs">
{global.total_tasks} tasks · {global.total_sessions} sessions ·{" "}
{formatDuration(global.total_duration_ms)}
</span>
</div>
<div className="ml-auto flex items-center gap-2">
<ToggleGroup
type="single"
value={range}
onValueChange={(v) => {
if (v) onRangeChange(v as RangeKey);
}}
variant="outline"
className="h-7"
>
{(["week", "month", "all"] as RangeKey[]).map((key) => (
<ToggleGroupItem
key={key}
value={key}
className="cursor-pointer h-7 px-2 font-mono text-[11px] data-[state=on]:bg-muted data-[state=on]:text-foreground"
>
{getRangeLabel(key)}
</ToggleGroupItem>
))}
</ToggleGroup>
<Button
type="button"
variant="outline"
size="sm"
className="h-7 px-2 font-mono text-[11px] cursor-pointer"
onClick={onCopy}
>
{copied ? "Copied" : "Copy Stats"}
</Button>
</div>
</header>
<PageTopbar
title="Statistics"
subtitle={`${global.total_tasks} tasks · ${global.total_sessions} sessions · ${formatDuration(global.total_duration_ms)}`}
actions={
<>
<ToggleGroup
type="single"
value={range}
onValueChange={(v) => {
if (v) onRangeChange(v as RangeKey);
}}
variant="outline"
className="h-7"
>
{(["week", "month", "all"] as RangeKey[]).map((key) => (
<ToggleGroupItem
key={key}
value={key}
className="cursor-pointer h-7 px-2 text-xs data-[state=on]:bg-muted data-[state=on]:text-foreground"
>
{getRangeLabel(key)}
</ToggleGroupItem>
))}
</ToggleGroup>
<Button
type="button"
variant="outline"
size="sm"
className="h-7 px-2 text-xs cursor-pointer"
onClick={onCopy}
>
{copied ? "Copied" : "Copy Stats"}
</Button>
</>
}
/>
);
}
@@ -343,13 +322,7 @@ export function StatsPageClient({ stats, error, workspaceId, activeRange }: Stat
if (error)
return (
<div className="h-screen w-full flex flex-col bg-background">
<header className="flex items-center gap-3 p-4 pb-3">
<Link href="/" className="text-2xl font-bold hover:opacity-80 cursor-pointer">
KanDev
</Link>
<span className="text-muted-foreground">/</span>
<span className="text-muted-foreground">Statistics</span>
</header>
<PageTopbar title="Statistics" />
<div className="flex-1 flex items-center justify-center">
<p className="text-destructive">Error loading stats: {error}</p>
</div>