Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/details/detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function DetailPageLayout({
}) {
return (
<div className="h-full overflow-auto">
<div className="mx-auto grid max-w-7xl gap-16 px-6 py-10 xl:grid-cols-[minmax(0,1fr)_minmax(16rem,20rem)]">
<div className="mx-auto grid max-w-7xl gap-16 px-3 py-10 md:px-6 xl:grid-cols-[minmax(0,1fr)_minmax(16rem,20rem)]">
<div className="flex min-w-0 flex-col gap-8">{main}</div>
{sidebar}
</div>
Expand Down Expand Up @@ -85,7 +85,7 @@ export function DetailPageSkeletonLayout({
}) {
return (
<div className="h-full overflow-auto">
<div className="mx-auto grid max-w-7xl gap-16 px-6 py-10 xl:grid-cols-[minmax(0,1fr)_minmax(16rem,20rem)]">
<div className="mx-auto grid max-w-7xl gap-16 px-3 py-10 md:px-6 xl:grid-cols-[minmax(0,1fr)_minmax(16rem,20rem)]">
<div className="flex min-w-0 flex-col gap-8">{main}</div>
<aside className="flex h-fit flex-col gap-6 xl:sticky xl:top-10">
{Array.from(
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/issues/issue-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const IssueRow = memo(function IssueRow({
<IssuesIcon size={16} strokeWidth={2} />
</div>
<div className="min-w-0 flex-1 flex flex-col gap-1">
<p className="truncate text-sm font-medium">{issue.title}</p>
<p className="text-sm font-medium md:truncate">{issue.title}</p>
<p className="flex items-center gap-1 truncate text-xs text-muted-foreground">
{issue.repository.fullName} #{issue.number}
{issue.author && (
Expand Down
10 changes: 10 additions & 0 deletions apps/dashboard/src/components/layouts/dashboard-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { useGitHubRevalidation } from "#/lib/use-github-revalidation";
import { useHasMounted } from "#/lib/use-has-mounted";
import { DashboardBottomBar } from "./dashboard-bottombar";
import { DashboardMobileNav } from "./dashboard-mobile-nav";
import { DashboardTopbar } from "./dashboard-topbar";

const CommandPalette = lazy(() =>
Expand Down Expand Up @@ -70,6 +71,15 @@ export function DashboardLayout() {
</div>
</div>
<DashboardBottomBar />
<DashboardMobileNav
user={user}
tabsReady={tabsReady}
counts={{
pulls: pullCount,
issues: issueCount,
reviews: pullsQuery.data?.reviewRequested.length,
}}
/>
<Suspense>
<CommandPalette />
<GitHubAccessDialog userId={user.id} />
Expand Down
181 changes: 181 additions & 0 deletions apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {
GitPullRequestIcon,
HomeIcon,
IssuesIcon,
MoonIcon,
ReviewsIcon,
SunIcon,
SystemIcon,
} from "@diffkit/icons";
import { Avatar, AvatarFallback } from "@diffkit/ui/components/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@diffkit/ui/components/dropdown-menu";
import { cn } from "@diffkit/ui/lib/utils";
import { Link } from "@tanstack/react-router";
import { useTheme } from "next-themes";
import { useState } from "react";
import { signOutToLogin } from "#/lib/auth-actions";

const themeOptions = [
{ value: "light", icon: SunIcon, label: "Light" },
{ value: "dark", icon: MoonIcon, label: "Dark" },
{ value: "system", icon: SystemIcon, label: "System" },
] as const;

interface MobileNavItem {
to: string;
label: string;
icon: typeof HomeIcon;
count?: number;
}

interface DashboardMobileNavProps {
user: {
name?: string | null;
email: string;
image?: string | null;
};
tabsReady: boolean;
counts: {
pulls?: number;
issues?: number;
reviews?: number;
};
}

export function DashboardMobileNav({
user,
tabsReady,
counts,
}: DashboardMobileNavProps) {
const { theme, setTheme } = useTheme();
const [avatarLoadFailed, setAvatarLoadFailed] = useState(false);

const displayName = user.name ?? user.email;
const initials = displayName
.split(" ")
.map((part) => part[0])
.join("")
.slice(0, 2)
.toUpperCase();

const navItems: MobileNavItem[] = [
{ to: "/", label: "Overview", icon: HomeIcon },
{
to: "/pulls",
label: "Pulls",
icon: GitPullRequestIcon,
count: counts.pulls,
},
{ to: "/issues", label: "Issues", icon: IssuesIcon, count: counts.issues },
{
to: "/reviews",
label: "Reviews",
icon: ReviewsIcon,
count: counts.reviews,
},
];

return (
<nav className="flex items-stretch border-t border-border bg-card md:hidden">
{navItems.map((item) => (
<Link
key={item.to}
to={item.to}
preload={false}
activeOptions={{ exact: true }}
activeProps={{ className: "active" }}
className={cn(
"relative flex flex-1 items-center justify-center py-3 text-muted-foreground transition-colors",
"[&.active]:bg-surface-1 [&.active]:text-foreground",
!tabsReady && "pointer-events-none opacity-0",
)}
>
<div className="relative">
<item.icon size={22} strokeWidth={1.8} />
{typeof item.count === "number" && item.count > 0 && (
<span className="absolute -right-4 -top-1.5 flex size-4 items-center justify-center rounded-full bg-border text-[9px] font-medium leading-none text-muted-foreground tabular-nums">
{item.count > 99 ? "+" : item.count}
</span>
)}
</div>
</Link>
))}

<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex flex-1 items-center justify-center py-3 text-muted-foreground"
>
<Avatar className="size-6 border border-border">
{user.image && !avatarLoadFailed ? (
<img
src={user.image}
alt={displayName}
className="size-full object-cover"
onError={() => setAvatarLoadFailed(true)}
/>
) : (
<AvatarFallback className="text-[8px]">
{initials}
</AvatarFallback>
)}
</Avatar>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="top" className="w-56">
<DropdownMenuLabel className="flex items-center justify-between">
<div>
<p>{displayName}</p>
<p className="font-normal text-muted-foreground">{user.email}</p>
</div>
<div className="flex items-center gap-0.5 rounded-md border border-border/50 p-0.5">
{themeOptions.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setTheme(opt.value)}
className={cn(
"flex size-6 items-center justify-center rounded-sm transition-colors",
theme === opt.value
? "bg-surface-1 text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
title={opt.label}
>
<opt.icon size={13} strokeWidth={2} />
</button>
))}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut keys={["G", "P"]} />
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut keys={["G", "S"]} />
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => void signOutToLogin()}>
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

<div className="pb-[env(safe-area-inset-bottom)]" />
</nav>
);
}
22 changes: 20 additions & 2 deletions apps/dashboard/src/components/layouts/dashboard-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) {
: "pointer-events-none -translate-y-0.5 opacity-0"
}`}
>
<div className="h-4 shrink-0 border-l border-border/50" />
<div className="hidden h-4 shrink-0 border-l border-border/50 md:block" />
<div className="relative min-w-0 flex-1 overflow-hidden">
<div
className={`pointer-events-none absolute inset-y-0 left-0 z-10 w-6 bg-gradient-to-r from-muted to-transparent transition-opacity ${canScrollLeft ? "opacity-100" : "opacity-0"}`}
Expand Down Expand Up @@ -180,14 +180,32 @@ const DetailTab = memo(function DetailTab({
#{tab.number}
</span>
)}
{/* Mobile: inline close button in flow — oversized touch target */}
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onClose(tab.id, tab.url);
}}
className="absolute inset-y-0 right-0 flex items-center rounded-r-md bg-surface-1 pl-1.5 pr-1.5 opacity-0 transition-opacity group-hover:opacity-100"
className="-mr-1.5 flex size-8 shrink-0 items-center justify-center rounded-md md:hidden"
aria-label={`Close ${tab.title}`}
>
<CloseIcon
size={12}
strokeWidth={2}
className="text-muted-foreground"
/>
</button>
{/* Desktop: overlay close button on hover */}
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onClose(tab.id, tab.url);
}}
className="absolute inset-y-0 right-0 hidden items-center rounded-r-md bg-surface-1 pl-1.5 pr-1.5 opacity-0 transition-opacity group-hover:opacity-100 md:flex"
aria-label={`Close ${tab.title}`}
>
<span className="absolute inset-y-0 -left-3 w-3 bg-gradient-to-r from-transparent to-surface-1" />
Expand Down
Loading
Loading