Add PWA meta tags for iOS home screen. Fix mobile properties drawer with safe area insets. Add image attachment button to comment thread. Improve sidebar with collapsible sections, project grouping, and mobile bottom nav. Show token and billing type breakdown on costs page. Fix inbox loading state to show content progressively. Various mobile overflow and layout fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
131 lines
4.2 KiB
TypeScript
131 lines
4.2 KiB
TypeScript
import { useMemo } from "react";
|
|
import { NavLink, useLocation } from "react-router-dom";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import {
|
|
House,
|
|
CircleDot,
|
|
SquarePen,
|
|
Users,
|
|
Inbox,
|
|
} from "lucide-react";
|
|
import { sidebarBadgesApi } from "../api/sidebarBadges";
|
|
import { useCompany } from "../context/CompanyContext";
|
|
import { useDialog } from "../context/DialogContext";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
import { cn } from "../lib/utils";
|
|
|
|
interface MobileBottomNavProps {
|
|
visible: boolean;
|
|
}
|
|
|
|
interface MobileNavLinkItem {
|
|
type: "link";
|
|
to: string;
|
|
label: string;
|
|
icon: typeof House;
|
|
badge?: number;
|
|
}
|
|
|
|
interface MobileNavActionItem {
|
|
type: "action";
|
|
label: string;
|
|
icon: typeof SquarePen;
|
|
onClick: () => void;
|
|
}
|
|
|
|
type MobileNavItem = MobileNavLinkItem | MobileNavActionItem;
|
|
|
|
export function MobileBottomNav({ visible }: MobileBottomNavProps) {
|
|
const location = useLocation();
|
|
const { selectedCompanyId } = useCompany();
|
|
const { openNewIssue } = useDialog();
|
|
|
|
const { data: sidebarBadges } = useQuery({
|
|
queryKey: queryKeys.sidebarBadges(selectedCompanyId!),
|
|
queryFn: () => sidebarBadgesApi.get(selectedCompanyId!),
|
|
enabled: !!selectedCompanyId,
|
|
});
|
|
|
|
const items = useMemo<MobileNavItem[]>(
|
|
() => [
|
|
{ type: "link", to: "/dashboard", label: "Home", icon: House },
|
|
{ type: "link", to: "/issues", label: "Issues", icon: CircleDot },
|
|
{ type: "action", label: "Create", icon: SquarePen, onClick: () => openNewIssue() },
|
|
{ type: "link", to: "/agents/all", label: "Agents", icon: Users },
|
|
{
|
|
type: "link",
|
|
to: "/inbox",
|
|
label: "Inbox",
|
|
icon: Inbox,
|
|
badge: sidebarBadges?.inbox,
|
|
},
|
|
],
|
|
[openNewIssue, sidebarBadges?.inbox],
|
|
);
|
|
|
|
return (
|
|
<nav
|
|
className={cn(
|
|
"fixed bottom-0 left-0 right-0 z-30 border-t border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/85 transition-transform duration-200 ease-out md:hidden pb-[env(safe-area-inset-bottom)]",
|
|
visible ? "translate-y-0" : "translate-y-full",
|
|
)}
|
|
aria-label="Mobile navigation"
|
|
>
|
|
<div className="grid h-16 grid-cols-5 px-1">
|
|
{items.map((item) => {
|
|
if (item.type === "action") {
|
|
const Icon = item.icon;
|
|
const active = location.pathname.startsWith("/issues/new");
|
|
return (
|
|
<button
|
|
key={item.label}
|
|
type="button"
|
|
onClick={item.onClick}
|
|
className={cn(
|
|
"relative flex min-w-0 flex-col items-center justify-center gap-1 rounded-md text-[10px] font-medium transition-colors",
|
|
active
|
|
? "text-foreground"
|
|
: "text-muted-foreground hover:text-foreground",
|
|
)}
|
|
>
|
|
<Icon className="h-[18px] w-[18px]" />
|
|
<span className="truncate">{item.label}</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
const Icon = item.icon;
|
|
return (
|
|
<NavLink
|
|
key={item.label}
|
|
to={item.to}
|
|
className={({ isActive }) =>
|
|
cn(
|
|
"relative flex min-w-0 flex-col items-center justify-center gap-1 rounded-md text-[10px] font-medium transition-colors",
|
|
isActive
|
|
? "text-foreground"
|
|
: "text-muted-foreground hover:text-foreground",
|
|
)
|
|
}
|
|
>
|
|
{({ isActive }) => (
|
|
<>
|
|
<span className="relative">
|
|
<Icon className={cn("h-[18px] w-[18px]", isActive && "stroke-[2.3]")} />
|
|
{item.badge != null && item.badge > 0 && (
|
|
<span className="absolute -right-2 -top-2 rounded-full bg-primary px-1.5 py-0.5 text-[10px] leading-none text-primary-foreground">
|
|
{item.badge > 99 ? "99+" : item.badge}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<span className="truncate">{item.label}</span>
|
|
</>
|
|
)}
|
|
</NavLink>
|
|
);
|
|
})}
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|