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
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