feat(portal): implement two-tier AppShell header (fix top-left wrapping) and interactive Storybook workbench
This commit is contained in:
1 parent
4ca86da55f
commit
6b589fa62a
11 files changed
+935
-178
No files matched your search
@@ -5,12 +5,16 @@ import { ServiceCard } from './components/ServiceCard';
|
|||||||
import { SshShelf } from './components/SshShelf';
|
import { SshShelf } from './components/SshShelf';
|
||||||
import { CommandPalette } from './components/CommandPalette';
|
import { CommandPalette } from './components/CommandPalette';
|
||||||
import { AgyTerminalModal } from './components/AgyTerminalModal';
|
import { AgyTerminalModal } from './components/AgyTerminalModal';
|
||||||
|
import { StorybookWorkbench } from './components/StorybookWorkbench';
|
||||||
|
import { LAYOUT_PRESETS } from './design-system/tokens';
|
||||||
import type { ServiceGroup } from './types/telemetry';
|
import type { ServiceGroup } from './types/telemetry';
|
||||||
|
|
||||||
export const App: React.FC = () => {
|
export const App: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
telemetry,
|
telemetry,
|
||||||
toast,
|
toast,
|
||||||
|
viewMode,
|
||||||
|
layoutPreset,
|
||||||
fetchTelemetry,
|
fetchTelemetry,
|
||||||
setShowCmdPalette,
|
setShowCmdPalette,
|
||||||
getEffectiveMode,
|
getEffectiveMode,
|
||||||
@@ -41,6 +45,8 @@ export const App: React.FC = () => {
|
|||||||
return '443 免端口 HTTPS 直达 (*.961504.xyz)';
|
return '443 免端口 HTTPS 直达 (*.961504.xyz)';
|
||||||
}, [getEffectiveMode, routeMode]);
|
}, [getEffectiveMode, routeMode]);
|
||||||
|
|
||||||
|
const gridClass = LAYOUT_PRESETS[layoutPreset]?.gridClass || LAYOUT_PRESETS['homepage-3col'].gridClass;
|
||||||
|
|
||||||
const serviceGroups = useMemo<ServiceGroup[]>(() => {
|
const serviceGroups = useMemo<ServiceGroup[]>(() => {
|
||||||
const t = telemetry;
|
const t = telemetry;
|
||||||
return [
|
return [
|
||||||
@@ -176,10 +182,13 @@ export const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 1. Top Utility & System Resources Ribbon */}
|
{/* 1. Standard Two-Tier AppShell Header & Telemetry Subheader */}
|
||||||
<TopRibbon />
|
<TopRibbon />
|
||||||
|
|
||||||
{/* 2. Main Categorized 3-Column Rows */}
|
{/* 2. View Switch: Dashboard vs Storybook Workbench */}
|
||||||
|
{viewMode === 'storybook' ? (
|
||||||
|
<StorybookWorkbench />
|
||||||
|
) : (
|
||||||
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-7">
|
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-7">
|
||||||
{serviceGroups.map((group) => (
|
{serviceGroups.map((group) => (
|
||||||
<section key={group.title} className="space-y-3">
|
<section key={group.title} className="space-y-3">
|
||||||
@@ -208,7 +217,7 @@ export const App: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className={`grid ${gridClass}`}>
|
||||||
{group.items.map((card) => (
|
{group.items.map((card) => (
|
||||||
<ServiceCard key={card.id} card={card} />
|
<ServiceCard key={card.id} card={card} />
|
||||||
))}
|
))}
|
||||||
@@ -219,6 +228,7 @@ export const App: React.FC = () => {
|
|||||||
{/* 3. Bottom SSH Quick Copy & Maintenance Shelf */}
|
{/* 3. Bottom SSH Quick Copy & Maintenance Shelf */}
|
||||||
<SshShelf />
|
<SshShelf />
|
||||||
</main>
|
</main>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 4. Global Cmd+K Command Palette */}
|
{/* 4. Global Cmd+K Command Palette */}
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
|
|||||||
@@ -0,0 +1,388 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { BookOpen, Check, Layers, Layout, Palette, Sparkles, Terminal } from 'lucide-react';
|
||||||
|
import { LAYOUT_PRESETS, THEME_PALETTES, type LayoutPresetId } from '../design-system/tokens';
|
||||||
|
import { usePortalStore } from '../store/usePortalStore';
|
||||||
|
import { ServiceCard } from './ServiceCard';
|
||||||
|
import { SshShelf } from './SshShelf';
|
||||||
|
|
||||||
|
type StoryId = 'layout-architecture' | 'theme-tokens' | 'service-card' | 'ssh-shelf';
|
||||||
|
|
||||||
|
export const StorybookWorkbench: React.FC = () => {
|
||||||
|
const [activeStory, setActiveStory] = useState<StoryId>('layout-architecture');
|
||||||
|
const {
|
||||||
|
theme,
|
||||||
|
resolvedTheme,
|
||||||
|
layoutPreset,
|
||||||
|
setTheme,
|
||||||
|
setLayoutPreset,
|
||||||
|
setViewMode,
|
||||||
|
telemetry,
|
||||||
|
} = usePortalStore();
|
||||||
|
|
||||||
|
// Interactive Playground Props for ServiceCard story
|
||||||
|
const [demoOnline, setDemoOnline] = useState(true);
|
||||||
|
const [demoAction, setDemoAction] = useState<'drawer' | 'backup' | undefined>('drawer');
|
||||||
|
|
||||||
|
const stories: Array<{ id: StoryId; group: string; title: string; icon: React.ReactNode }> = [
|
||||||
|
{
|
||||||
|
id: 'layout-architecture',
|
||||||
|
group: 'FOUNDATIONS / 架构与布局',
|
||||||
|
title: '01. AppShell 双层顶栏与网格布局',
|
||||||
|
icon: <Layout className="w-3.5 h-3.5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'theme-tokens',
|
||||||
|
group: 'FOUNDATIONS / 架构与布局',
|
||||||
|
title: '02. 双主题色板与自动切换规范',
|
||||||
|
icon: <Palette className="w-3.5 h-3.5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'service-card',
|
||||||
|
group: 'COMPONENTS / 核心组件',
|
||||||
|
title: '03. ServiceCard 内联遥测卡片',
|
||||||
|
icon: <Layers className="w-3.5 h-3.5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ssh-shelf',
|
||||||
|
group: 'COMPONENTS / 核心组件',
|
||||||
|
title: '04. SshShelf 快捷指令栏',
|
||||||
|
icon: <Terminal className="w-3.5 h-3.5" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 items-start">
|
||||||
|
{/* Left Sidebar: Storybook Navigation Tree */}
|
||||||
|
<aside className="hl-card p-4 space-y-4 lg:sticky lg:top-20">
|
||||||
|
<div className="flex items-center justify-between border-b hl-border pb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<BookOpen className="w-4 h-4" style={{ color: 'var(--accent)' }} />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wider">Storybook 组件库</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-mono-num px-1.5 py-0.5 rounded hl-muted">v3.1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
{stories.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.id}
|
||||||
|
onClick={() => setActiveStory(s.id)}
|
||||||
|
className="w-full px-3 py-2 rounded-lg text-left text-xs font-medium flex items-center gap-2.5 transition-all cursor-pointer"
|
||||||
|
style={
|
||||||
|
activeStory === s.id
|
||||||
|
? {
|
||||||
|
backgroundColor: 'var(--accent-soft)',
|
||||||
|
color: 'var(--accent)',
|
||||||
|
}
|
||||||
|
: { color: 'var(--text-muted)' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{s.icon}
|
||||||
|
<span className="truncate">{s.title}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-3 border-t hl-border space-y-2 text-xs">
|
||||||
|
<div className="text-[11px] font-semibold" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
全局外观状态预览
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-1 p-1 rounded-lg hl-muted border hl-border">
|
||||||
|
{(['system', 'light', 'dark'] as const).map((t) => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
onClick={() => setTheme(t)}
|
||||||
|
className="py-1 rounded text-[11px] font-medium cursor-pointer"
|
||||||
|
style={
|
||||||
|
theme === t
|
||||||
|
? { backgroundColor: 'var(--bg-card)', color: 'var(--text-main)' }
|
||||||
|
: { color: 'var(--text-muted)' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t === 'system' ? '自动' : t === 'light' ? '浅色' : '深色'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('dashboard')}
|
||||||
|
className="w-full hl-btn-primary py-2 text-xs text-center mt-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
返回中控台主页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Right Canvas: Interactive Story Preview & Controls */}
|
||||||
|
<div className="lg:col-span-3 space-y-6">
|
||||||
|
{/* STORY 1: Layout Architecture */}
|
||||||
|
{activeStory === 'layout-architecture' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="hl-card p-6 space-y-4">
|
||||||
|
<div className="flex items-center justify-between border-b hl-border pb-4">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-mono-num uppercase tracking-wider"
|
||||||
|
style={{ color: 'var(--accent)' }}
|
||||||
|
>
|
||||||
|
Foundations / AppShell Layout
|
||||||
|
</span>
|
||||||
|
<h2 className="text-lg font-bold mt-0.5">
|
||||||
|
标准双层顶栏架构 (Two-Tier AppShell) & 响应式网格切换
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs leading-relaxed" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
参考 <strong>Vercel Geist / shadcn/ui Dashboard-01 / GitHub AppShell</strong>{' '}
|
||||||
|
标准规范,将原本容易挤压折行的单层顶栏拆分为职责清晰的双层结构:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Visual Blueprint Diagram of Two-Tier Header */}
|
||||||
|
<div className="p-4 rounded-xl hl-muted border hl-border space-y-2.5 font-mono-num text-xs">
|
||||||
|
<div className="p-3 rounded-lg hl-card flex items-center justify-between">
|
||||||
|
<span>[左] 🍎 HomeLab · 导航切换 (中控台 / Storybook)</span>
|
||||||
|
<span style={{ color: 'var(--accent)' }}>
|
||||||
|
[最右固定] 🔍 搜索 ⌘K · >_ AGY 终端 · ☀️/🌙 主题切换
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 rounded-lg hl-card flex items-center justify-between">
|
||||||
|
<span style={{ color: 'var(--success)' }}>
|
||||||
|
[左] 实时硬件遥测条 (M6 CPU · RAM · Z4 20TB · SG 专线)
|
||||||
|
</span>
|
||||||
|
<span>[右对齐] 路由模式切换器 (智能路由 / 443 / 10443 / LAN)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Grid Preset Selector */}
|
||||||
|
<div className="pt-2 space-y-3">
|
||||||
|
<div className="text-xs font-semibold">
|
||||||
|
选择主页卡片网格布局预设(点击立即应用到主页):
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3.5">
|
||||||
|
{(Object.keys(LAYOUT_PRESETS) as LayoutPresetId[]).map((key) => {
|
||||||
|
const preset = LAYOUT_PRESETS[key];
|
||||||
|
const active = layoutPreset === key;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
onClick={() => setLayoutPreset(key)}
|
||||||
|
className="hl-card hl-card-interactive p-4 cursor-pointer space-y-2"
|
||||||
|
style={active ? { borderColor: 'var(--accent)' } : {}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-bold">{preset.name}</span>
|
||||||
|
{active && (
|
||||||
|
<span
|
||||||
|
className="w-4 h-4 rounded-full flex items-center justify-center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--accent)',
|
||||||
|
color: '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check className="w-2.5 h-2.5" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px]" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
{preset.description}
|
||||||
|
</p>
|
||||||
|
<code
|
||||||
|
className="text-[10px] font-mono-num block pt-1"
|
||||||
|
style={{ color: 'var(--accent)' }}
|
||||||
|
>
|
||||||
|
{preset.gridClass}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STORY 2: Theme Tokens */}
|
||||||
|
{activeStory === 'theme-tokens' && (
|
||||||
|
<div className="hl-card p-6 space-y-6">
|
||||||
|
<div className="border-b hl-border pb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-mono-num uppercase tracking-wider"
|
||||||
|
style={{ color: 'var(--accent)' }}
|
||||||
|
>
|
||||||
|
Foundations / Color System
|
||||||
|
</span>
|
||||||
|
<h2 className="text-lg font-bold mt-0.5">
|
||||||
|
主流双主题色板规范(当前生效:{resolvedTheme === 'dark' ? '深色极客' : '浅色纯净'})
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-mono-num px-2.5 py-1 rounded hl-muted">
|
||||||
|
模式: {theme === 'system' ? '跟随设备自动切换' : '手动锁定'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
{(['light', 'dark'] as const).map((modeKey) => {
|
||||||
|
const p = THEME_PALETTES[modeKey];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className="rounded-xl p-5 space-y-4 border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: p.tokens['--bg-page'],
|
||||||
|
color: p.tokens['--text-main'],
|
||||||
|
borderColor: p.tokens['--border-color'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between border-b pb-3"
|
||||||
|
style={{ borderColor: p.tokens['--border-color'] }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-bold">{p.name}</div>
|
||||||
|
<div className="text-xs mt-0.5" style={{ color: p.tokens['--text-muted'] }}>
|
||||||
|
{p.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setTheme(modeKey)}
|
||||||
|
className="px-2.5 py-1 rounded-md text-xs font-mono-num font-semibold cursor-pointer"
|
||||||
|
style={{
|
||||||
|
backgroundColor: p.tokens['--primary'],
|
||||||
|
color: p.tokens['--primary-fg'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
预览此主题
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{Object.entries(p.tokens).map(([tokenName, hex]) => (
|
||||||
|
<div
|
||||||
|
key={tokenName}
|
||||||
|
className="p-2 rounded-lg border flex items-center gap-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: p.tokens['--bg-card'],
|
||||||
|
borderColor: p.tokens['--border-color'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-5 h-5 rounded border shrink-0"
|
||||||
|
style={{
|
||||||
|
backgroundColor: hex,
|
||||||
|
borderColor: p.tokens['--border-hover'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-[10px] font-mono-num font-semibold truncate">
|
||||||
|
{tokenName}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-[10px] font-mono-num truncate"
|
||||||
|
style={{ color: p.tokens['--text-muted'] }}
|
||||||
|
>
|
||||||
|
{hex}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STORY 3: ServiceCard Interactive Playground */}
|
||||||
|
{activeStory === 'service-card' && (
|
||||||
|
<div className="hl-card p-6 space-y-6">
|
||||||
|
<div className="border-b hl-border pb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-mono-num uppercase tracking-wider"
|
||||||
|
style={{ color: 'var(--accent)' }}
|
||||||
|
>
|
||||||
|
Components / ServiceCard
|
||||||
|
</span>
|
||||||
|
<h2 className="text-lg font-bold mt-0.5">
|
||||||
|
三段式内联遥测卡片 (ServiceCard Interactive Story)
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<Sparkles className="w-4 h-4" style={{ color: 'var(--accent)' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Controls */}
|
||||||
|
<div className="p-4 rounded-xl hl-muted border hl-border flex flex-wrap items-center gap-6 text-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold">在线状态 (online):</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setDemoOnline(!demoOnline)}
|
||||||
|
className="px-2.5 py-1 rounded hl-card font-mono-num cursor-pointer"
|
||||||
|
>
|
||||||
|
{demoOnline ? 'true (绿灯)' : 'false (离线红灯)'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold">右上角动作按钮 (actionType):</span>
|
||||||
|
{(['drawer', 'backup', undefined] as const).map((act, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={() => setDemoAction(act)}
|
||||||
|
className="px-2.5 py-1 rounded hl-card font-mono-num cursor-pointer"
|
||||||
|
style={demoAction === act ? { borderColor: 'var(--accent)' } : {}}
|
||||||
|
>
|
||||||
|
{act || 'route-badge'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live Component Canvas */}
|
||||||
|
<div className="max-w-md mx-auto py-4">
|
||||||
|
<ServiceCard
|
||||||
|
card={{
|
||||||
|
id: 'agy',
|
||||||
|
title: 'AGY 云端工作台',
|
||||||
|
subtitle: 'agy.961504.xyz · ttyd + tmux',
|
||||||
|
iconUrl:
|
||||||
|
'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/google-gemini.png',
|
||||||
|
online: demoOnline,
|
||||||
|
actionType: demoAction,
|
||||||
|
desc: 'M6 Mac mini 上的 24 小时常驻 AI 编程终端,已同步 Google 登录态并锁定新加坡出口,关掉网页后台继续运行。',
|
||||||
|
edgeUrl: 'https://agy.961504.xyz',
|
||||||
|
homeUrl: 'https://agy.home.961504.xyz:10443',
|
||||||
|
lanUrl: 'http://192.168.31.164:7681',
|
||||||
|
stats: [
|
||||||
|
{ label: 'M6 CPU 负载', value: `${telemetry.mini.cpu_pct}%` },
|
||||||
|
{ label: '已用内存', value: `${telemetry.mini.mem_used_gb} / 24 GB` },
|
||||||
|
{ label: 'tmux 会话', value: '常驻运行中', highlight: true },
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* STORY 4: SshShelf */}
|
||||||
|
{activeStory === 'ssh-shelf' && (
|
||||||
|
<div className="hl-card p-6 space-y-6">
|
||||||
|
<div className="border-b hl-border pb-4">
|
||||||
|
<span
|
||||||
|
className="text-[11px] font-mono-num uppercase tracking-wider"
|
||||||
|
style={{ color: 'var(--accent)' }}
|
||||||
|
>
|
||||||
|
Components / SshShelf
|
||||||
|
</span>
|
||||||
|
<h2 className="text-lg font-bold mt-0.5">底部 SSH 快捷复制与运维操作栏</h2>
|
||||||
|
</div>
|
||||||
|
<SshShelf />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Moon, Search, Sun, Terminal } from 'lucide-react';
|
import { BookOpen, LayoutGrid, Moon, Search, Sun, Terminal } from 'lucide-react';
|
||||||
import { usePortalStore } from '../store/usePortalStore';
|
import { usePortalStore } from '../store/usePortalStore';
|
||||||
import type { RouteModeId } from '../types/telemetry';
|
import type { RouteModeId } from '../types/telemetry';
|
||||||
|
|
||||||
@@ -15,23 +15,34 @@ export const TopRibbon: React.FC = () => {
|
|||||||
theme,
|
theme,
|
||||||
resolvedTheme,
|
resolvedTheme,
|
||||||
routeMode,
|
routeMode,
|
||||||
|
viewMode,
|
||||||
telemetry,
|
telemetry,
|
||||||
setTheme,
|
setTheme,
|
||||||
toggleTheme,
|
toggleTheme,
|
||||||
setRouteMode,
|
setRouteMode,
|
||||||
|
setViewMode,
|
||||||
setShowAgyModal,
|
setShowAgyModal,
|
||||||
setShowCmdPalette,
|
setShowCmdPalette,
|
||||||
} = usePortalStore();
|
} = usePortalStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{/* =====================================================================
|
||||||
|
Tier 1: Standard AppShell Global Top Navbar (h-14, NEVER wraps!)
|
||||||
|
Left: Brand + View Tabs (中控台 / Storybook)
|
||||||
|
Far Right (Red Box): Search (Cmd+K) + AGY Terminal + Theme Switcher
|
||||||
|
===================================================================== */}
|
||||||
<header
|
<header
|
||||||
className="border-b hl-border sticky top-0 z-30 backdrop-blur-md"
|
className="border-b hl-border sticky top-0 z-30 backdrop-blur-md"
|
||||||
style={{ backgroundColor: 'color-mix(in srgb, var(--bg-page) 86%, transparent)' }}
|
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 + Navigation Tabs */}
|
||||||
|
<div className="flex items-center gap-4 shrink-0">
|
||||||
|
<div
|
||||||
|
onClick={() => setViewMode('dashboard')}
|
||||||
|
className="flex items-center gap-2.5 cursor-pointer"
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 flex flex-wrap items-center justify-between gap-3">
|
|
||||||
{/* Left: Brand + Live System Micro-Gauges Ribbon */}
|
|
||||||
<div className="flex flex-wrap items-center gap-2.5 sm:gap-3">
|
|
||||||
<div className="flex items-center gap-2.5 pr-1">
|
|
||||||
<img
|
<img
|
||||||
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/apple.png"
|
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/apple.png"
|
||||||
alt="Mac mini"
|
alt="Mac mini"
|
||||||
@@ -45,66 +56,54 @@ export const TopRibbon: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden xl:flex items-center gap-2 text-xs font-mono-num">
|
<div className="h-4 w-[1px] hl-muted" />
|
||||||
{/* CPU Gauge */}
|
|
||||||
<div className="hl-pill px-2.5 py-1 flex items-center gap-2" title="Apple M6 12核处理器负载">
|
{/* View Switcher Tabs: Dashboard vs Storybook */}
|
||||||
<span style={{ color: 'var(--text-muted)' }}>M6 CPU</span>
|
<nav className="flex items-center gap-1 text-xs">
|
||||||
<div className="w-12 h-1.5 rounded-full hl-muted overflow-hidden">
|
<button
|
||||||
<div
|
onClick={() => setViewMode('dashboard')}
|
||||||
className="h-full hl-bar-fill"
|
className="px-2.5 py-1.5 rounded-md font-medium flex items-center gap-1.5 transition-all cursor-pointer"
|
||||||
style={{ width: `${Math.max(5, telemetry.mini.cpu_pct)}%` }}
|
style={
|
||||||
/>
|
viewMode === 'dashboard'
|
||||||
</div>
|
? {
|
||||||
<span className="font-medium">{telemetry.mini.cpu_pct}%</span>
|
backgroundColor: 'var(--bg-muted)',
|
||||||
|
color: 'var(--text-main)',
|
||||||
|
}
|
||||||
|
: { color: 'var(--text-muted)' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LayoutGrid className="w-3.5 h-3.5" />
|
||||||
|
<span>中控台</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('storybook')}
|
||||||
|
className="px-2.5 py-1.5 rounded-md font-medium flex items-center gap-1.5 transition-all cursor-pointer"
|
||||||
|
style={
|
||||||
|
viewMode === 'storybook'
|
||||||
|
? {
|
||||||
|
backgroundColor: 'var(--bg-muted)',
|
||||||
|
color: 'var(--text-main)',
|
||||||
|
}
|
||||||
|
: { color: 'var(--text-muted)' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BookOpen className="w-3.5 h-3.5" />
|
||||||
|
<span>Storybook 设计规范</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* RAM Gauge */}
|
{/* Far Right Group: Pinned to Right Edge (Search + AGY + Theme Switcher) */}
|
||||||
<div className="hl-pill px-2.5 py-1 flex items-center gap-2" title="Mac mini 24GB 统一内存">
|
<div className="flex items-center gap-2.5 shrink-0 ml-auto">
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* ZSpace 20TB Gauge */}
|
|
||||||
<div className="hl-pill px-2.5 py-1 flex items-center gap-2" title="极空间 Z4 20TB 存储阵列">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* SG AI Line Gauge */}
|
|
||||||
<div className="hl-pill px-2.5 py-1 flex items-center gap-1.5" title="AGY 锁定新加坡 Azure 专线出口">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right: Search (Cmd+K), Route Switcher, AGY Terminal, Top-Right Theme Switcher */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCmdPalette(true)}
|
onClick={() => setShowCmdPalette(true)}
|
||||||
className="hl-btn-outline px-2.5 py-1 text-xs flex items-center gap-3 cursor-pointer"
|
className="hl-btn-outline px-3 py-1.5 text-xs flex items-center gap-3 cursor-pointer"
|
||||||
title="按 Cmd+K 唤起全局命令面板"
|
title="按 Cmd+K 唤起全局命令面板"
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-1.5" style={{ color: 'var(--text-muted)' }}>
|
<span className="flex items-center gap-1.5" style={{ color: 'var(--text-muted)' }}>
|
||||||
<Search className="w-3.5 h-3.5" />
|
<Search className="w-3.5 h-3.5" />
|
||||||
<span className="hidden sm:inline">搜索...</span>
|
<span className="hidden sm:inline">搜索服务或指令...</span>
|
||||||
</span>
|
</span>
|
||||||
<kbd
|
<kbd
|
||||||
className="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted"
|
className="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted"
|
||||||
@@ -114,27 +113,6 @@ export const TopRibbon: React.FC = () => {
|
|||||||
</kbd>
|
</kbd>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="hidden md:flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs">
|
|
||||||
{MODES.map((m) => (
|
|
||||||
<button
|
|
||||||
key={m.id}
|
|
||||||
onClick={() => setRouteMode(m.id)}
|
|
||||||
className="px-2 py-1 rounded-md font-medium transition-all cursor-pointer"
|
|
||||||
style={
|
|
||||||
routeMode === m.id
|
|
||||||
? {
|
|
||||||
backgroundColor: 'var(--bg-card)',
|
|
||||||
color: 'var(--text-main)',
|
|
||||||
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
|
|
||||||
}
|
|
||||||
: { color: 'var(--text-muted)' }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{m.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAgyModal(true)}
|
onClick={() => setShowAgyModal(true)}
|
||||||
className="hl-btn-primary px-3 py-1.5 text-xs flex items-center gap-1.5 cursor-pointer"
|
className="hl-btn-primary px-3 py-1.5 text-xs flex items-center gap-1.5 cursor-pointer"
|
||||||
@@ -143,7 +121,7 @@ export const TopRibbon: React.FC = () => {
|
|||||||
<span>AGY 终端</span>
|
<span>AGY 终端</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Top-Right Mainstream Theme Switcher (Auto System + Light/Dark 2 Mainstream Themes) */}
|
{/* 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">
|
<div className="flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs">
|
||||||
<button
|
<button
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
@@ -153,17 +131,17 @@ export const TopRibbon: React.FC = () => {
|
|||||||
color: 'var(--text-main)',
|
color: 'var(--text-main)',
|
||||||
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
|
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
|
||||||
}}
|
}}
|
||||||
title="点击切换深色 / 浅色主题"
|
title="点击切换深色 / 浅色主题(默认跟随系统)"
|
||||||
>
|
>
|
||||||
{resolvedTheme === 'dark' ? (
|
{resolvedTheme === 'dark' ? (
|
||||||
<>
|
<>
|
||||||
<Moon className="w-3.5 h-3.5" />
|
<Moon className="w-3.5 h-3.5" />
|
||||||
<span className="hidden sm:inline">深色</span>
|
<span>深色</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Sun className="w-3.5 h-3.5" />
|
<Sun className="w-3.5 h-3.5" />
|
||||||
<span className="hidden sm:inline">浅色</span>
|
<span>浅色</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -181,5 +159,88 @@ export const TopRibbon: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{/* =====================================================================
|
||||||
|
Tier 2: Subheader Telemetry & Route Mode Bar (Aligned with Grid)
|
||||||
|
Left: 4 Hardware & Network Gauges
|
||||||
|
Right: Route Mode Segmented Switcher (智能路由 / 443 / 10443 / LAN)
|
||||||
|
===================================================================== */}
|
||||||
|
{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 (Pinned to Right Edge of Subheader) */}
|
||||||
|
<div className="flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs ml-auto">
|
||||||
|
{MODES.map((m) => (
|
||||||
|
<button
|
||||||
|
key={m.id}
|
||||||
|
onClick={() => setRouteMode(m.id)}
|
||||||
|
className="px-2.5 py-1 rounded-md font-medium transition-all cursor-pointer"
|
||||||
|
style={
|
||||||
|
routeMode === m.id
|
||||||
|
? {
|
||||||
|
backgroundColor: 'var(--bg-card)',
|
||||||
|
color: 'var(--text-main)',
|
||||||
|
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
|
||||||
|
}
|
||||||
|
: { color: 'var(--text-muted)' }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{m.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { THEME_PALETTES, LAYOUT_PRESETS } from './tokens';
|
||||||
|
|
||||||
|
export const DesignTokensShowcase: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="p-6 sm:p-8 space-y-10 max-w-6xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="space-y-2 border-b hl-border pb-5">
|
||||||
|
<div className="text-xs font-mono-num uppercase tracking-widest" style={{ color: 'var(--accent)' }}>
|
||||||
|
HomeLab Design System · Storybook v3.0
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">主题色彩、排版与布局规范 (Design Tokens)</h1>
|
||||||
|
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
全系统仅保留两套高标准主流主题(浅色纯净 Light & 深色极客 Dark),默认通过{' '}
|
||||||
|
<code className="font-mono-num px-1.5 py-0.5 rounded hl-muted">prefers-color-scheme</code>{' '}
|
||||||
|
自动跟随设备切换,并支持右上角一键手动覆盖。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Side-by-Side Theme Palettes */}
|
||||||
|
<section className="space-y-4">
|
||||||
|
<h2 className="text-base font-semibold">1. 双主题色彩变量对照表 (Dual Mainstream Palettes)</h2>
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{(['light', 'dark'] as const).map((key) => {
|
||||||
|
const p = THEME_PALETTES[key];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className="rounded-xl p-5 space-y-4 border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: p.tokens['--bg-page'],
|
||||||
|
color: p.tokens['--text-main'],
|
||||||
|
borderColor: p.tokens['--border-color'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b pb-3" style={{ borderColor: p.tokens['--border-color'] }}>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-bold">{p.name}</div>
|
||||||
|
<div className="text-xs mt-0.5" style={{ color: p.tokens['--text-muted'] }}>
|
||||||
|
{p.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="px-2.5 py-1 rounded-md text-xs font-mono-num font-semibold"
|
||||||
|
style={{
|
||||||
|
backgroundColor: p.tokens['--primary'],
|
||||||
|
color: p.tokens['--primary-fg'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.id.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2.5">
|
||||||
|
{Object.entries(p.tokens).map(([tokenName, hex]) => (
|
||||||
|
<div
|
||||||
|
key={tokenName}
|
||||||
|
className="p-2.5 rounded-lg border flex items-center gap-2.5"
|
||||||
|
style={{
|
||||||
|
backgroundColor: p.tokens['--bg-card'],
|
||||||
|
borderColor: p.tokens['--border-color'],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-6 h-6 rounded-md border shrink-0"
|
||||||
|
style={{
|
||||||
|
backgroundColor: hex,
|
||||||
|
borderColor: p.tokens['--border-hover'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-[11px] font-mono-num font-semibold truncate">{tokenName}</div>
|
||||||
|
<div className="text-[10px] font-mono-num truncate" style={{ color: p.tokens['--text-muted'] }}>
|
||||||
|
{hex}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Typography Scale */}
|
||||||
|
<section className="space-y-4">
|
||||||
|
<h2 className="text-base font-semibold">2. 字体与等宽数字规范 (Typography & Tabular Numbers)</h2>
|
||||||
|
<div className="hl-card p-5 space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono-num uppercase" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
UI Sans Font · Inter / SF Pro Display
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold">AGY 云端工作台 · 极空间 Z4 私有云</div>
|
||||||
|
<p className="text-xs leading-relaxed" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
正文描述统一采用 12px (0.75rem) 搭配 1.625 行高,限制最最多 2 行截断 (line-clamp-2),确保所有卡片高度严格对齐。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono-num uppercase" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
Telemetry Mono Font · JetBrains Mono (tabular-nums)
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold font-mono-num" style={{ color: 'var(--success)' }}>
|
||||||
|
16.58 TB FREE · 68 ms · 104.43.105.243
|
||||||
|
</div>
|
||||||
|
<p className="text-xs leading-relaxed" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
所有实时跳动的 CPU 百分比、内存、存储余量、网络延迟强制启用{' '}
|
||||||
|
<code className="font-mono-num">font-variant-numeric: tabular-nums</code>,杜绝每 3 秒刷新时产生像素抖动。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Layout Grid Presets */}
|
||||||
|
<section className="space-y-4">
|
||||||
|
<h2 className="text-base font-semibold">3. 响应式布局网格预设 (Layout Grid Presets)</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{Object.values(LAYOUT_PRESETS).map((lp) => (
|
||||||
|
<div key={lp.id} className="hl-card p-4 space-y-2">
|
||||||
|
<div className="text-sm font-semibold">{lp.name}</div>
|
||||||
|
<code className="text-[11px] font-mono-num block" style={{ color: 'var(--accent)' }}>
|
||||||
|
{lp.gridClass}
|
||||||
|
</code>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
{lp.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
export interface ThemePalette {
|
||||||
|
id: 'light' | 'dark';
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
tokens: {
|
||||||
|
'--bg-page': string;
|
||||||
|
'--bg-card': string;
|
||||||
|
'--bg-widget': string;
|
||||||
|
'--bg-muted': string;
|
||||||
|
'--bg-hover': string;
|
||||||
|
'--text-main': string;
|
||||||
|
'--text-muted': string;
|
||||||
|
'--text-subtle': string;
|
||||||
|
'--border-color': string;
|
||||||
|
'--border-hover': string;
|
||||||
|
'--primary': string;
|
||||||
|
'--primary-fg': string;
|
||||||
|
'--accent': string;
|
||||||
|
'--accent-soft': string;
|
||||||
|
'--success': string;
|
||||||
|
'--success-soft': string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const THEME_PALETTES: Record<'light' | 'dark', ThemePalette> = {
|
||||||
|
light: {
|
||||||
|
id: 'light',
|
||||||
|
name: 'Shadcn Clean Light (浅色纯净)',
|
||||||
|
description: '对标 Apple / Stripe / Linear 浅色日光模式,高对比深灰文字配纯净白卡片与冷灰微边框。',
|
||||||
|
tokens: {
|
||||||
|
'--bg-page': '#f8fafc',
|
||||||
|
'--bg-card': '#ffffff',
|
||||||
|
'--bg-widget': '#f8fafc',
|
||||||
|
'--bg-muted': '#f1f5f9',
|
||||||
|
'--bg-hover': '#ffffff',
|
||||||
|
'--text-main': '#0f172a',
|
||||||
|
'--text-muted': '#64748b',
|
||||||
|
'--text-subtle': '#94a3b8',
|
||||||
|
'--border-color': '#e2e8f0',
|
||||||
|
'--border-hover': '#cbd5e1',
|
||||||
|
'--primary': '#0f172a',
|
||||||
|
'--primary-fg': '#ffffff',
|
||||||
|
'--accent': '#2563eb',
|
||||||
|
'--accent-soft': 'rgba(37, 99, 235, 0.08)',
|
||||||
|
'--success': '#10b981',
|
||||||
|
'--success-soft': 'rgba(16, 185, 129, 0.10)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
id: 'dark',
|
||||||
|
name: 'Catppuccin / Linear Dark (深色极客)',
|
||||||
|
description: '融合 Catppuccin Mocha 与 Linear 暗黑规范,柔和护眼的深邃蓝黑基底配高辨识度状态色。',
|
||||||
|
tokens: {
|
||||||
|
'--bg-page': '#11111b',
|
||||||
|
'--bg-card': '#1e1e2e',
|
||||||
|
'--bg-widget': '#181825',
|
||||||
|
'--bg-muted': '#313244',
|
||||||
|
'--bg-hover': '#242438',
|
||||||
|
'--text-main': '#cdd6f4',
|
||||||
|
'--text-muted': '#a6adc8',
|
||||||
|
'--text-subtle': '#6c7086',
|
||||||
|
'--border-color': '#313244',
|
||||||
|
'--border-hover': '#45475a',
|
||||||
|
'--primary': '#cba6f7',
|
||||||
|
'--primary-fg': '#11111b',
|
||||||
|
'--accent': '#89b4fa',
|
||||||
|
'--accent-soft': 'rgba(137, 180, 250, 0.15)',
|
||||||
|
'--success': '#a6e3a1',
|
||||||
|
'--success-soft': 'rgba(166, 227, 161, 0.15)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LayoutPresetId = 'homepage-3col' | 'bento-split' | 'compact-2col';
|
||||||
|
|
||||||
|
export interface LayoutPreset {
|
||||||
|
id: LayoutPresetId;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
gridClass: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LAYOUT_PRESETS: Record<LayoutPresetId, LayoutPreset> = {
|
||||||
|
'homepage-3col': {
|
||||||
|
id: 'homepage-3col',
|
||||||
|
name: 'Homepage 经典三列矩阵 (3-Column Grid)',
|
||||||
|
description: 'gethomepage.dev 标志性对称 3 列分组卡片,底部内嵌 3 格实时遥测单元。',
|
||||||
|
gridClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4',
|
||||||
|
},
|
||||||
|
'bento-split': {
|
||||||
|
id: 'bento-split',
|
||||||
|
name: 'Shadcn 双列宽屏卡片 (2-Column Wide Bento)',
|
||||||
|
description: '适合大字号阅读的 2 列宽体卡片布局,留白更充裕。',
|
||||||
|
gridClass: 'grid-cols-1 lg:grid-cols-2 gap-5',
|
||||||
|
},
|
||||||
|
'compact-2col': {
|
||||||
|
id: 'compact-2col',
|
||||||
|
name: '高密度四列紧凑矩阵 (4-Column Dense)',
|
||||||
|
description: '适合超宽显示器的一屏高密度 4 列布局。',
|
||||||
|
gridClass: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3.5',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { RouteModeId, TelemetryData, ThemeId } from '../types/telemetry';
|
import type { RouteModeId, TelemetryData, ThemeId } from '../types/telemetry';
|
||||||
|
import type { LayoutPresetId } from '../design-system/tokens';
|
||||||
|
|
||||||
interface PortalState {
|
interface PortalState {
|
||||||
theme: ThemeId;
|
theme: ThemeId;
|
||||||
resolvedTheme: 'light' | 'dark';
|
resolvedTheme: 'light' | 'dark';
|
||||||
routeMode: RouteModeId;
|
routeMode: RouteModeId;
|
||||||
|
layoutPreset: LayoutPresetId;
|
||||||
|
viewMode: 'dashboard' | 'storybook';
|
||||||
showAgyModal: boolean;
|
showAgyModal: boolean;
|
||||||
showCmdPalette: boolean;
|
showCmdPalette: boolean;
|
||||||
toast: string;
|
toast: string;
|
||||||
@@ -13,6 +16,8 @@ interface PortalState {
|
|||||||
setTheme: (theme: ThemeId) => void;
|
setTheme: (theme: ThemeId) => void;
|
||||||
toggleTheme: () => void;
|
toggleTheme: () => void;
|
||||||
setRouteMode: (mode: RouteModeId) => void;
|
setRouteMode: (mode: RouteModeId) => void;
|
||||||
|
setLayoutPreset: (preset: LayoutPresetId) => void;
|
||||||
|
setViewMode: (view: 'dashboard' | 'storybook') => void;
|
||||||
setShowAgyModal: (open: boolean) => void;
|
setShowAgyModal: (open: boolean) => void;
|
||||||
setShowCmdPalette: (open: boolean) => void;
|
setShowCmdPalette: (open: boolean) => void;
|
||||||
showToast: (msg: string) => void;
|
showToast: (msg: string) => void;
|
||||||
@@ -43,11 +48,19 @@ const initialTheme: ThemeId =
|
|||||||
: 'system';
|
: 'system';
|
||||||
const initialResolved = applyDomTheme(initialTheme);
|
const initialResolved = applyDomTheme(initialTheme);
|
||||||
const initialRouteMode = (localStorage.getItem('hl_route_mode') as RouteModeId) || 'auto';
|
const initialRouteMode = (localStorage.getItem('hl_route_mode') as RouteModeId) || 'auto';
|
||||||
|
const initialLayout =
|
||||||
|
(localStorage.getItem('hl_layout_preset') as LayoutPresetId) || 'homepage-3col';
|
||||||
|
const initialView =
|
||||||
|
typeof window !== 'undefined' && window.location.pathname.startsWith('/storybook')
|
||||||
|
? 'storybook'
|
||||||
|
: 'dashboard';
|
||||||
|
|
||||||
export const usePortalStore = create<PortalState>((set, get) => ({
|
export const usePortalStore = create<PortalState>((set, get) => ({
|
||||||
theme: initialTheme,
|
theme: initialTheme,
|
||||||
resolvedTheme: initialResolved,
|
resolvedTheme: initialResolved,
|
||||||
routeMode: initialRouteMode,
|
routeMode: initialRouteMode,
|
||||||
|
layoutPreset: initialLayout,
|
||||||
|
viewMode: initialView,
|
||||||
showAgyModal: false,
|
showAgyModal: false,
|
||||||
showCmdPalette: false,
|
showCmdPalette: false,
|
||||||
toast: '',
|
toast: '',
|
||||||
@@ -111,6 +124,19 @@ export const usePortalStore = create<PortalState>((set, get) => ({
|
|||||||
set({ routeMode });
|
set({ routeMode });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setLayoutPreset: (layoutPreset) => {
|
||||||
|
localStorage.setItem('hl_layout_preset', layoutPreset);
|
||||||
|
set({ layoutPreset });
|
||||||
|
},
|
||||||
|
|
||||||
|
setViewMode: (viewMode) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const nextUrl = viewMode === 'storybook' ? '/storybook' : '/';
|
||||||
|
window.history.pushState({}, '', nextUrl);
|
||||||
|
}
|
||||||
|
set({ viewMode });
|
||||||
|
},
|
||||||
|
|
||||||
setShowAgyModal: (showAgyModal) => set({ showAgyModal }),
|
setShowAgyModal: (showAgyModal) => set({ showAgyModal }),
|
||||||
setShowCmdPalette: (showCmdPalette) => set({ showCmdPalette }),
|
setShowCmdPalette: (showCmdPalette) => set({ showCmdPalette }),
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user