From 31c7fa2e70eefdc084c20ef8d7f50afcb1fa4897 Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 10 Apr 2026 21:43:12 -0400 Subject: [PATCH] Add mobile-first responsive layout with native bottom nav and drawer patterns - Add mobile bottom navigation bar with icon-only nav items and avatar - Hide desktop-only controls (avatar menu, more button, nav items) on mobile - Make detail tabs mobile-friendly: inline close button, hidden divider - Wrap PR/issue titles on mobile instead of truncating - Reduce page padding on mobile across pulls, issues, reviews, and detail pages - Hide preview button and timeline guide line on mobile - Make PR stats bar wrap naturally on small screens - Add Vaul drawer component for native-feeling mobile interactions - Convert Dialog to render as bottom drawer on mobile (<768px) - Convert review page: file sidebar as drawer, submit review as drawer - Force unified diff style on mobile by hiding split/unified toggle --- .../src/components/details/detail-page.tsx | 4 +- .../src/components/issues/issue-row.tsx | 2 +- .../components/layouts/dashboard-layout.tsx | 10 + .../layouts/dashboard-mobile-nav.tsx | 181 ++++++++++++ .../src/components/layouts/dashboard-tabs.tsx | 22 +- .../components/layouts/dashboard-topbar.tsx | 144 ++++----- .../pulls/detail/pull-detail-activity.tsx | 2 +- .../pulls/detail/pull-detail-header.tsx | 36 ++- .../src/components/pulls/pull-request-row.tsx | 4 +- .../components/pulls/review/review-page.tsx | 158 +++++++--- .../pulls/review/review-submit-popover.tsx | 278 ++++++++++-------- .../src/routes/_protected/issues.tsx | 2 +- .../dashboard/src/routes/_protected/pulls.tsx | 2 +- .../src/routes/_protected/reviews.tsx | 2 +- packages/ui/package.json | 3 +- packages/ui/src/components/dialog.tsx | 94 ++++++ packages/ui/src/components/drawer.tsx | 125 ++++++++ pnpm-lock.yaml | 178 +++++++---- 18 files changed, 928 insertions(+), 319 deletions(-) create mode 100644 apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx create mode 100644 packages/ui/src/components/drawer.tsx diff --git a/apps/dashboard/src/components/details/detail-page.tsx b/apps/dashboard/src/components/details/detail-page.tsx index d1792a9..e8aa074 100644 --- a/apps/dashboard/src/components/details/detail-page.tsx +++ b/apps/dashboard/src/components/details/detail-page.tsx @@ -17,7 +17,7 @@ export function DetailPageLayout({ }) { return (
-
+
{main}
{sidebar}
@@ -85,7 +85,7 @@ export function DetailPageSkeletonLayout({ }) { return (
-
+
{main}
-

{issue.title}

+

{issue.title}

{issue.repository.fullName} #{issue.number} {issue.author && ( diff --git a/apps/dashboard/src/components/layouts/dashboard-layout.tsx b/apps/dashboard/src/components/layouts/dashboard-layout.tsx index 1dcc87b..d53a4f4 100644 --- a/apps/dashboard/src/components/layouts/dashboard-layout.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-layout.tsx @@ -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(() => @@ -70,6 +71,15 @@ export function DashboardLayout() {

+ diff --git a/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx b/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx new file mode 100644 index 0000000..a39ae50 --- /dev/null +++ b/apps/dashboard/src/components/layouts/dashboard-mobile-nav.tsx @@ -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 ( + + ); +} diff --git a/apps/dashboard/src/components/layouts/dashboard-tabs.tsx b/apps/dashboard/src/components/layouts/dashboard-tabs.tsx index c5004d9..2423a4e 100644 --- a/apps/dashboard/src/components/layouts/dashboard-tabs.tsx +++ b/apps/dashboard/src/components/layouts/dashboard-tabs.tsx @@ -105,7 +105,7 @@ export function DashboardTabs({ tabsReady, routerRef }: DashboardTabsProps) { : "pointer-events-none -translate-y-0.5 opacity-0" }`} > -
+
)} + {/* Mobile: inline close button in flow — oversized touch target */} + {/* Desktop: overlay close button on hover */} + - - - -
-

{displayName}

-

{user.email}

-
-
- {themeOptions.map((opt) => ( - - ))} -
-
- - - - Profile - - - - Settings - +
+ + + + + + +
+

{displayName}

+

+ {user.email} +

+
+
+ {themeOptions.map((opt) => ( + + ))} +
+
+ + + + Profile + + + + Settings + + + + + { + void signOutToLogin(); + }} + > + Sign out - - - { - void signOutToLogin(); - }} - > - Sign out - -
-
+ + +
@@ -270,7 +276,7 @@ export function DashboardTopbar({
-
+
-

{pr.title}

+

{pr.title}

{pr.repository.fullName} #{pr.number} {pr.author && ( @@ -97,7 +97,7 @@ export const PullRequestRow = memo(function PullRequestRow({ {formatRelativeTime(pr.updatedAt)}

-
+
+ )} + #{pr.number} -
+
-
+
{pr.title}
-
+
{sidebarFileCount} - {" "} - {sidebarFileCount === 1 ? "file" : "files"} + + + {" "} + {sidebarFileCount === 1 ? "file" : "files"} + +{diffStats.totalAdditions} @@ -443,7 +507,7 @@ const ReviewToolbar = memo(function ReviewToolbar({
-
+
-
+
void) => { + const mql = window.matchMedia(MD_QUERY); + mql.addEventListener("change", cb); + return () => mql.removeEventListener("change", cb); +}; +const getMdSnapshot = () => window.matchMedia(MD_QUERY).matches; +const getMdServerSnapshot = () => true; + +function useIsDesktop() { + return useSyncExternalStore(mdSubscribe, getMdSnapshot, getMdServerSnapshot); +} + +const reviewOptions: Array<{ + value: ReviewEvent; + label: string; + description: string; + icon: typeof CommentIcon; + color: string; +}> = [ + { + value: "COMMENT", + label: "Comment", + description: "Submit general feedback without explicit approval.", + icon: CommentIcon, + color: "text-foreground", + }, + { + value: "APPROVE", + label: "Approve", + description: "Submit feedback and approve merging these changes.", + icon: TickIcon, + color: "text-green-500", + }, + { + value: "REQUEST_CHANGES", + label: "Request changes", + description: "Submit feedback suggesting changes.", + icon: GitBranchIcon, + color: "text-red-500", + }, +]; + export function ReviewSubmitPopover({ pendingCount, isSubmitting, @@ -27,6 +75,7 @@ export function ReviewSubmitPopover({ const [body, setBody] = useState(""); const [event, setEvent] = useState("COMMENT"); const [isOpen, setIsOpen] = useState(false); + const isDesktop = useIsDesktop(); const handleSubmit = () => { onSubmit(body, event); @@ -34,43 +83,98 @@ export function ReviewSubmitPopover({ setIsOpen(false); }; - const reviewOptions: Array<{ - value: ReviewEvent; - label: string; - description: string; - icon: typeof CommentIcon; - color: string; - }> = [ - { - value: "COMMENT", - label: "Comment", - description: "Submit general feedback without explicit approval.", - icon: CommentIcon, - color: "text-foreground", - }, - { - value: "APPROVE", - label: "Approve", - description: "Submit feedback and approve merging these changes.", - icon: TickIcon, - color: "text-green-500", - }, - { - value: "REQUEST_CHANGES", - label: "Request changes", - description: "Submit feedback suggesting changes.", - icon: GitBranchIcon, - color: "text-red-500", - }, - ]; + const trigger = ( + + ); - return ( - - + const form = ( +
+
+

Finish your review

+ {isDesktop && ( + + )} +
+ +
+