Skip to content
Merged
8 changes: 4 additions & 4 deletions web/src/components/layout/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ export function AppLayout() {
const timeoutSeconds = parseInt(settings?.force_project_timeout || '30', 10);

return (
<SidebarProvider>
<SidebarProvider className="h-svh! min-h-0! overflow-hidden">
<AppSidebar />
<SidebarInset>
<SidebarInset className="flex flex-col">
{/* Mobile header with sidebar trigger */}
<header className="flex h-12 items-center gap-2 border-b px-4 md:hidden">
<header className="flex h-12 shrink-0 items-center gap-2 border-b px-4 md:hidden">
<SidebarTrigger />
</header>
<div className="@container/main h-full">
<div className="@container/main flex-1 min-h-0 overflow-hidden">
<Outlet />
</div>
</SidebarInset>
Expand Down
60 changes: 60 additions & 0 deletions web/src/components/layout/app-sidebar/animated-nav-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { NavLink, useLocation } from 'react-router-dom';
import { StreamingBadge } from '@/components/ui/streaming-badge';
import { MarqueeBackground } from '@/components/ui/marquee-background';
import { SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { cn } from '@/lib/utils';
import type { ReactNode } from 'react';

interface AnimatedNavItemProps {
/** The route path to navigate to */
to: string;
/** Function to check if the route is active */
isActive: (pathname: string) => boolean;
/** Tooltip text */
tooltip: string;
/** Icon element */
icon: ReactNode;
/** Label text */
label: string;
/** Streaming count for badge */
streamingCount: number;
/** Color for marquee and badge */
color: string;
}

/**
* Reusable navigation item with marquee background and streaming badge
*/
export function AnimatedNavItem({
to,
isActive: isActiveFn,
tooltip,
icon,
label,
streamingCount,
color,
}: AnimatedNavItemProps) {
const location = useLocation();
const isActive = isActiveFn(location.pathname);

return (
<SidebarMenuItem>
<SidebarMenuButton
render={<NavLink to={to} />}
isActive={isActive}
tooltip={tooltip}
className={cn(
'relative overflow-hidden',
isActive && 'bg-transparent! hover:bg-sidebar-accent/50!',
)}
>
<MarqueeBackground show={streamingCount > 0} color={color} opacity={0.3} />
<span className="relative z-10">{icon}</span>
<span className="relative z-10">{label}</span>
</SidebarMenuButton>
<SidebarMenuBadge>
<StreamingBadge count={streamingCount} color={color} />
</SidebarMenuBadge>
</SidebarMenuItem>
);
}
33 changes: 11 additions & 22 deletions web/src/components/layout/app-sidebar/client-routes-items.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,33 @@
import { NavLink, useLocation } from 'react-router-dom';
import {
ClientIcon,
allClientTypes,
getClientName,
getClientColor,
} from '@/components/icons/client-icons';
import { StreamingBadge } from '@/components/ui/streaming-badge';
import { MarqueeBackground } from '@/components/ui/marquee-background';
import { useStreamingRequests } from '@/hooks/use-streaming';
import type { ClientType } from '@/lib/transport';
import { SidebarMenuButton, SidebarMenuItem, SidebarMenuBadge } from '@/components/ui/sidebar';
import { AnimatedNavItem } from './animated-nav-item';

function ClientNavItem({
clientType,
streamingCount
streamingCount,
}: {
clientType: ClientType;
streamingCount: number;
}) {
const location = useLocation();
const color = getClientColor(clientType);
const clientName = getClientName(clientType);
const isActive = location.pathname === `/routes/${clientType}`;

return (
<SidebarMenuItem>
<SidebarMenuButton
render={<NavLink to={`/routes/${clientType}`} />}
isActive={isActive}
tooltip={clientName}
className="relative overflow-hidden"
>
<MarqueeBackground show={streamingCount > 0 && !isActive} color={color} opacity={0.5} />
<ClientIcon type={clientType} size={18} className="relative z-10" />
<span className="relative z-10">{clientName}</span>
</SidebarMenuButton>
<SidebarMenuBadge>
<StreamingBadge count={streamingCount} color={color} />
</SidebarMenuBadge>
</SidebarMenuItem>
<AnimatedNavItem
to={`/routes/${clientType}`}
isActive={(pathname) => pathname === `/routes/${clientType}`}
tooltip={clientName}
icon={<ClientIcon type={clientType} size={18} />}
label={clientName}
streamingCount={streamingCount}
color={color}
/>
);
}

Expand Down
5 changes: 1 addition & 4 deletions web/src/components/layout/app-sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function AppSidebar() {
const versionDisplay = proxyStatus?.version ?? '...';

return (
<Sidebar collapsible="icon">
<Sidebar collapsible="icon" className="border-border">
<SidebarHeader>
<NavProxyStatus />
</SidebarHeader>
Expand All @@ -37,6 +37,3 @@ export function AppSidebar() {
</Sidebar>
);
}

// Alias for backwards compatibility
export { AppSidebar as SidebarNav };
32 changes: 10 additions & 22 deletions web/src/components/layout/app-sidebar/requests-nav-item.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
import { NavLink, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Activity } from 'lucide-react';
import { StreamingBadge } from '@/components/ui/streaming-badge';
import { MarqueeBackground } from '@/components/ui/marquee-background';
import { useStreamingRequests } from '@/hooks/use-streaming';
import { SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { AnimatedNavItem } from './animated-nav-item';

/**
* Requests navigation item with streaming badge and marquee animation
*/
export function RequestsNavItem() {
const location = useLocation();
const { total } = useStreamingRequests();
const { t } = useTranslation();
const isActive =
location.pathname === '/requests' || location.pathname.startsWith('/requests/');
const color = 'var(--color-success)'; // emerald-500

return (
<SidebarMenuItem>
<SidebarMenuButton
render={<NavLink to="/requests" />}
isActive={isActive}
tooltip={t('requests.title')}
className="relative"
>
<MarqueeBackground show={total > 0 && !isActive} color={color} opacity={0.4} />
<Activity className="relative z-10" />
<span className="relative z-10">{t('requests.title')}</span>
</SidebarMenuButton>
<SidebarMenuBadge>
<StreamingBadge count={total} color={color} />
</SidebarMenuBadge>
</SidebarMenuItem>
<AnimatedNavItem
to="/requests"
isActive={(pathname) => pathname === '/requests' || pathname.startsWith('/requests/')}
tooltip={t('requests.title')}
icon={<Activity />}
label={t('requests.title')}
streamingCount={total}
color={color}
/>
);
}
2 changes: 1 addition & 1 deletion web/src/components/layout/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { AppLayout } from './app-layout';
export { SidebarNav } from './app-sidebar';
export { AppSidebar } from './app-sidebar';
export { PageHeader } from './page-header';
export { NavProxyStatus } from './nav-proxy-status';
export { SidebarRenderer } from './app-sidebar/sidebar-renderer';
Expand Down
23 changes: 11 additions & 12 deletions web/src/components/layout/nav-proxy-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Radio, Check, Copy } from 'lucide-react';
import { useProxyStatus } from '@/hooks/queries';
import { useSidebar } from '@/components/ui/sidebar';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Button } from '../ui';
import { useTranslation } from 'react-i18next';

export function NavProxyStatus() {
Expand Down Expand Up @@ -60,20 +59,20 @@ export function NavProxyStatus() {
}

return (
<Button
variant={'ghost'}
onClick={handleCopy}
className="h-auto border-none p-2 flex items-center gap-2 group w-full rounded-lg transition-all cursor-pointer"
title={`Click to copy: ${fullUrl}`}
>
<div className="w-8 h-8 rounded-lg bg-emerald-400/10 flex items-center justify-center shrink-0 group-hover:bg-emerald-400/20 transition-colors">
<div className="h-auto border-none p-2 flex items-center gap-2 w-full rounded-lg transition-all group">
<div className="w-8 h-8 rounded-lg bg-emerald-400/10 flex items-center justify-center shrink-0 transition-colors cursor-default">
<Radio size={16} className="text-emerald-400" />
</div>
<div className="flex flex-col items-start flex-1 min-w-0">
<span className="text-caption text-text-muted">{t('proxy.listeningOn')}</span>
<span className="font-mono font-medium text-text-primary truncate">{proxyAddress}</span>
<span className="font-mono font-medium text-text-primary truncate">{proxyAddress}</span>
</div>
<div className="shrink-0 text-muted-foreground relative w-4 h-4">
<button
type="button"
onClick={handleCopy}
className="shrink-0 text-muted-foreground relative w-4 h-4 cursor-pointer hover:text-foreground transition-colors"
title={`Click to copy: ${fullUrl}`}
>
<Copy
size={14}
className={`absolute inset-0 transition-all ${
Expand All @@ -86,7 +85,7 @@ export function NavProxyStatus() {
copied ? 'scale-100 opacity-100' : 'scale-0 opacity-0'
}`}
/>
</div>
</Button>
</button>
</div>
);
}
Loading