Files
homelab/stacks/portal/web/src/components/TopRibbon.tsx
T

242 lines
9.6 KiB
TypeScript

import React from 'react';
import { BookOpen, Bot, LayoutGrid, MessageSquare, Moon, Search, Sun, Terminal } from 'lucide-react';
import { usePortalStore } from '../store/usePortalStore';
import type { RouteModeId } from '../types/telemetry';
import { Button } from './ui/button';
import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
const MODES: Array<{ id: RouteModeId; label: string }> = [
{ id: 'auto', label: '智能路由' },
{ id: 'edge443', label: '443 免端口' },
{ id: 'home10443', label: '10443 家宽' },
{ id: 'lan', label: 'LAN 局域网' },
];
export const TopRibbon: React.FC = () => {
const {
theme,
resolvedTheme,
routeMode,
viewMode,
telemetry,
setTheme,
toggleTheme,
setRouteMode,
setViewMode,
setShowAgyModal,
setShowWechatModal,
setShowCmdPalette,
} = usePortalStore();
return (
<>
{/* =====================================================================
Tier 1: Standard AppShell Global Top Navbar (h-14, NEVER wraps!)
Left: Brand + Radix Tabs (中控台 / Storybook)
Far Right: Search (Cmd+K) + AGY Terminal + Theme Switcher
===================================================================== */}
<header
className="border-b hl-border sticky top-0 z-30 backdrop-blur-md"
style={{ backgroundColor: 'color-mix(in srgb, var(--bg-card) 88%, transparent)' }}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-14 flex items-center justify-between gap-4">
{/* Left Group: Brand Logo + Official Radix Navigation Tabs */}
<div className="flex items-center gap-4 shrink-0">
<div
onClick={() => setViewMode('dashboard')}
className="flex items-center gap-2.5 cursor-pointer"
>
<img
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/apple.png"
alt="Mac mini"
className="w-6 h-6 rounded"
/>
<span className="font-semibold text-sm tracking-tight">HomeLab</span>
<span
className="w-2 h-2 rounded-full"
style={{ backgroundColor: 'var(--success)' }}
title="全屋 4 节点在线"
/>
</div>
<div className="h-4 w-[1px] hl-muted" />
{/* View Switcher Tabs via @radix-ui/react-tabs */}
<Tabs
value={viewMode}
onValueChange={(v) => setViewMode(v as 'dashboard' | 'agents' | 'storybook')}
>
<TabsList>
<TabsTrigger value="dashboard">
<LayoutGrid className="w-3.5 h-3.5" />
<span>中控台</span>
</TabsTrigger>
<TabsTrigger value="agents">
<Bot className="w-3.5 h-3.5" />
<span>Agent 编排</span>
</TabsTrigger>
<TabsTrigger value="storybook">
<BookOpen className="w-3.5 h-3.5" />
<span>Storybook 设计规范</span>
</TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* Far Right Group: Pinned to Right Edge (Search + AGY + Theme Switcher) */}
<div className="flex items-center gap-2.5 shrink-0 ml-auto">
<Button
variant="outline"
onClick={() => setShowCmdPalette(true)}
className="gap-3"
title="按 Cmd+K 唤起全局命令面板"
>
<span className="flex items-center gap-1.5" style={{ color: 'var(--text-muted)' }}>
<Search className="w-3.5 h-3.5" />
<span className="hidden sm:inline">搜索服务或指令...</span>
</span>
<kbd
className="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted"
style={{ color: 'var(--text-muted)' }}
>
⌘K
</kbd>
</Button>
<Button
variant="outline"
onClick={() => setShowWechatModal(true)}
title="连接微信 ClawBot,在微信内直接收发消息与控制 Mac mini"
>
<span
className="w-2 h-2 rounded-full"
style={{
backgroundColor: telemetry.services.clawbot ? 'var(--success)' : 'var(--accent)',
}}
/>
<MessageSquare className="w-3.5 h-3.5" />
<span>微信 ClawBot</span>
</Button>
<Button onClick={() => setShowAgyModal(true)}>
<Terminal className="w-3.5 h-3.5" />
<span>AGY 终端</span>
</Button>
{/* Top-Right Theme Toggle (Pinned at Far Right Corner!) */}
<div className="flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs">
<button
onClick={toggleTheme}
className="px-2.5 py-1 rounded-md font-medium transition-all flex items-center gap-1.5 cursor-pointer"
style={{
backgroundColor: 'var(--bg-card)',
color: 'var(--text-main)',
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
}}
title="点击切换深色 / 浅色主题(默认跟随系统)"
>
{resolvedTheme === 'dark' ? (
<>
<Moon className="w-3.5 h-3.5" />
<span>深色</span>
</>
) : (
<>
<Sun className="w-3.5 h-3.5" />
<span>浅色</span>
</>
)}
</button>
{theme !== 'system' && (
<button
onClick={() => setTheme('system')}
className="px-2 py-1 rounded-md text-[11px] transition-all cursor-pointer"
style={{ color: 'var(--text-muted)' }}
title="恢复跟随设备系统深浅色自动切换"
>
自动
</button>
)}
</div>
</div>
</div>
</header>
{/* =====================================================================
Tier 2: Subheader Telemetry & Route Mode Bar (Aligned with Grid)
Left: 4 Hardware & Network Gauges
Right: Route Mode Segmented Switcher via @radix-ui/react-tabs
===================================================================== */}
{viewMode === 'dashboard' && (
<div className="border-b hl-border" style={{ backgroundColor: 'var(--bg-card)' }}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2.5 flex flex-wrap items-center justify-between gap-3">
{/* Left: System Telemetry Strip */}
<div className="flex flex-wrap items-center gap-2 text-xs font-mono-num">
<div className="hl-pill px-2.5 py-1 flex items-center gap-2">
<span style={{ color: 'var(--text-muted)' }}>M6 CPU</span>
<div className="w-12 h-1.5 rounded-full hl-muted overflow-hidden">
<div
className="h-full hl-bar-fill"
style={{ width: `${Math.max(5, telemetry.mini.cpu_pct)}%` }}
/>
</div>
<span className="font-medium">{telemetry.mini.cpu_pct}%</span>
</div>
<div className="hl-pill px-2.5 py-1 flex items-center gap-2">
<span style={{ color: 'var(--text-muted)' }}>RAM</span>
<div className="w-12 h-1.5 rounded-full hl-muted overflow-hidden">
<div
className="h-full hl-bar-fill"
style={{ width: `${Math.max(5, telemetry.mini.mem_pct)}%` }}
/>
</div>
<span className="font-medium">{telemetry.mini.mem_used_gb}G</span>
</div>
<div className="hl-pill px-2.5 py-1 flex items-center gap-2">
<span style={{ color: 'var(--text-muted)' }}>NAS 余量</span>
<div className="w-12 h-1.5 rounded-full hl-muted overflow-hidden">
<div
className="h-full hl-bar-fill-green"
style={{ width: `${Math.max(5, telemetry.nas.pct)}%` }}
/>
</div>
<span className="font-medium" style={{ color: 'var(--success)' }}>
{telemetry.nas.free_tb} TB
</span>
</div>
<div className="hl-pill px-2.5 py-1 flex items-center gap-1.5">
<span
className="w-1.5 h-1.5 rounded-full"
style={{ backgroundColor: 'var(--success)' }}
/>
<span style={{ color: 'var(--text-muted)' }}>SG 专线</span>
<span className="font-medium">
{telemetry.network.sg_delay_ms > 0 ? `${telemetry.network.sg_delay_ms}ms` : '直连'}
</span>
</div>
</div>
{/* Right: Route Mode Switcher via @radix-ui/react-tabs */}
<Tabs
value={routeMode}
onValueChange={(v) => setRouteMode(v as RouteModeId)}
className="ml-auto"
>
<TabsList>
{MODES.map((m) => (
<TabsTrigger key={m.id} value={m.id}>
{m.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
</div>
)}
</>
);
};