feat(ui): integrate official shadcn/ui + Radix primitives + cmdk + cva in src/components/ui

Browse Source
This commit is contained in:
Yin Kou committed 2026-11-07 00:40:25 +08:00
1 parent cbc47336d6
commit 07d2050c5f
21 files changed
+1606 -471

No files matched your search

@@ -1,6 +1,8 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { usePortalStore } from '../store/usePortalStore'; import { usePortalStore } from '../store/usePortalStore';
import { AuthGateCard } from './AuthGateModal'; import { AuthGateCard } from './AuthGateModal';
import { Button } from './ui/button';
import { Dialog, DialogContent, DialogDescription, DialogTitle } from './ui/dialog';
export const AgyTerminalModal: React.FC = () => { export const AgyTerminalModal: React.FC = () => {
const { showAgyModal, setShowAgyModal, getEffectiveMode } = usePortalStore(); const { showAgyModal, setShowAgyModal, getEffectiveMode } = usePortalStore();
@@ -35,12 +37,15 @@ export const AgyTerminalModal: React.FC = () => {
}); });
}, [showAgyModal, mode]); }, [showAgyModal, mode]);
if (!showAgyModal) return null;
return ( return (
<div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-3 sm:p-6"> <Dialog open={showAgyModal} onOpenChange={setShowAgyModal}>
<div className="w-full h-full max-w-6xl hl-card flex flex-col overflow-hidden shadow-2xl"> <DialogContent className="w-[95vw] max-w-6xl h-[88vh] flex flex-col overflow-hidden p-0">
<div className="px-4 py-2.5 border-b hl-border flex items-center justify-between hl-muted"> <DialogTitle className="sr-only">AGY 云端工作台</DialogTitle>
<DialogDescription className="sr-only">
M6 Mac mini 上的 24 小时 tmux 常驻 AI 编程终端
</DialogDescription>
<div className="px-4 py-2.5 border-b hl-border flex items-center justify-between hl-muted shrink-0">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<img <img
src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/google-gemini.png" src="https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/google-gemini.png"
@@ -52,29 +57,34 @@ export const AgyTerminalModal: React.FC = () => {
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<a <Button variant="outline" size="sm" asChild>
href={url} <a
target="_blank" href={url}
rel="noopener noreferrer" target="_blank"
className="hl-btn-outline px-2.5 py-1 text-xs font-mono-num" rel="noopener noreferrer"
> className="font-mono-num"
新标签页全屏 ↗ >
</a> 新标签页全屏 ↗
<button </a>
onClick={() => setShowAgyModal(false)} </Button>
className="hl-btn-primary px-3 py-1 text-xs cursor-pointer" <Button size="sm" onClick={() => setShowAgyModal(false)}>
>
收起终端 收起终端
</button> </Button>
</div> </div>
</div> </div>
{!authChecked ? ( {!authChecked ? (
<div className="flex-1 flex items-center justify-center text-xs font-mono-num" style={{ color: 'var(--text-muted)' }}> <div
className="flex-1 flex items-center justify-center text-xs font-mono-num"
style={{ color: 'var(--text-muted)' }}
>
正在校验 HomeLab SSO 通行证... 正在校验 HomeLab SSO 通行证...
</div> </div>
) : !authenticated ? ( ) : !authenticated ? (
<div className="flex-1 flex items-center justify-center p-4" style={{ backgroundColor: 'var(--bg-page)' }}> <div
className="flex-1 flex items-center justify-center p-4"
style={{ backgroundColor: 'var(--bg-page)' }}
>
<AuthGateCard <AuthGateCard
targetUrl={url} targetUrl={url}
onSuccess={() => setAuthenticated(true)} onSuccess={() => setAuthenticated(true)}
@@ -82,9 +92,13 @@ export const AgyTerminalModal: React.FC = () => {
/> />
</div> </div>
) : ( ) : (
<iframe src={url} title="AGY Cloud Workbench" className="w-full flex-1 bg-black border-0" /> <iframe
src={url}
title="AGY Cloud Workbench"
className="w-full flex-1 bg-black border-0"
/>
)} )}
</div> </DialogContent>
</div> </Dialog>
); );
}; };
@@ -1,5 +1,10 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Check, Eye, EyeOff, Lock, ShieldCheck, Terminal, User } from 'lucide-react'; import { Eye, EyeOff, Lock, ShieldCheck, Terminal, User } from 'lucide-react';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Card } from './ui/card';
import { InputGroup } from './ui/input';
import { Switch } from './ui/switch';
interface AuthGateCardProps { interface AuthGateCardProps {
targetUrl?: string; targetUrl?: string;
@@ -50,11 +55,7 @@ export const AuthGateCard: React.FC<AuthGateCardProps> = ({
}; };
return ( return (
<div <Card className={`w-full max-w-md p-6 space-y-5 ${embedded ? '' : 'shadow-2xl'}`}>
className={`w-full max-w-md hl-card p-6 space-y-5 ${
embedded ? '' : 'shadow-2xl'
}`}
>
{/* Header */} {/* Header */}
<div className="flex items-start justify-between gap-3 border-b hl-border pb-4"> <div className="flex items-start justify-between gap-3 border-b hl-border pb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -67,14 +68,12 @@ export const AuthGateCard: React.FC<AuthGateCardProps> = ({
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="text-sm font-bold tracking-tight">HomeLab 统一安全通行证</h3> <h3 className="text-sm font-bold tracking-tight">HomeLab 统一安全通行证</h3>
<span <Badge variant="success">SSO 2.0</Badge>
className="text-[10px] font-mono-num px-1.5 py-0.5 rounded font-semibold"
style={{ backgroundColor: 'var(--success-soft)', color: 'var(--success)' }}
>
SSO 2.0
</span>
</div> </div>
<p className="text-xs font-mono-num mt-0.5 truncate max-w-[260px]" style={{ color: 'var(--text-muted)' }}> <p
className="text-xs font-mono-num mt-0.5 truncate max-w-[260px]"
style={{ color: 'var(--text-muted)' }}
>
{targetUrl} {targetUrl}
</p> </p>
</div> </div>
@@ -87,7 +86,7 @@ export const AuthGateCard: React.FC<AuthGateCardProps> = ({
<label className="block text-xs font-semibold" style={{ color: 'var(--text-muted)' }}> <label className="block text-xs font-semibold" style={{ color: 'var(--text-muted)' }}>
管理员账号 (Username) 管理员账号 (Username)
</label> </label>
<div className="hl-input-group"> <InputGroup>
<User className="w-4 h-4 shrink-0" style={{ color: 'var(--text-subtle)' }} /> <User className="w-4 h-4 shrink-0" style={{ color: 'var(--text-subtle)' }} />
<input <input
type="text" type="text"
@@ -97,7 +96,7 @@ export const AuthGateCard: React.FC<AuthGateCardProps> = ({
placeholder="输入管理员账号 (如 ixvii0iv)" placeholder="输入管理员账号 (如 ixvii0iv)"
className="font-mono-num" className="font-mono-num"
/> />
</div> </InputGroup>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
@@ -109,7 +108,7 @@ export const AuthGateCard: React.FC<AuthGateCardProps> = ({
与 Mac mini 密码一致 与 Mac mini 密码一致
</span> </span>
</div> </div>
<div className="hl-input-group"> <InputGroup>
<Lock className="w-4 h-4 shrink-0" style={{ color: 'var(--text-subtle)' }} /> <Lock className="w-4 h-4 shrink-0" style={{ color: 'var(--text-subtle)' }} />
<input <input
type={showPass ? 'text' : 'password'} type={showPass ? 'text' : 'password'}
@@ -129,36 +128,15 @@ export const AuthGateCard: React.FC<AuthGateCardProps> = ({
> >
{showPass ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />} {showPass ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button> </button>
</div> </InputGroup>
</div> </div>
{/* Custom Checkbox */} {/* Official @radix-ui/react-switch Toggle */}
<div className="flex items-center justify-between pt-0.5"> <div className="flex items-center justify-between pt-1">
<button <span className="text-xs select-none" style={{ color: 'var(--text-muted)' }}>
type="button" 在此设备保持 30 天免密通行 (*.961504.xyz)
onClick={() => setRememberMe(!rememberMe)} </span>
className="flex items-center gap-2 text-xs cursor-pointer select-none" <Switch checked={rememberMe} onCheckedChange={setRememberMe} />
style={{ color: 'var(--text-muted)' }}
>
<span
className="w-4 h-4 rounded flex items-center justify-center border transition-colors"
style={
rememberMe
? {
backgroundColor: 'var(--accent)',
borderColor: 'var(--accent)',
color: '#ffffff',
}
: {
backgroundColor: 'var(--bg-muted)',
borderColor: 'var(--border-color)',
}
}
>
{rememberMe && <Check className="w-3 h-3" />}
</span>
<span>在此设备保持 30 天免密通行 (*.961504.xyz)</span>
</button>
</div> </div>
{errorMsg && ( {errorMsg && (
@@ -177,24 +155,16 @@ export const AuthGateCard: React.FC<AuthGateCardProps> = ({
{/* Actions */} {/* Actions */}
<div className="flex items-center justify-end gap-2.5 pt-2"> <div className="flex items-center justify-end gap-2.5 pt-2">
{onCancel && ( {onCancel && (
<button <Button type="button" variant="outline" onClick={onCancel}>
type="button"
onClick={onCancel}
className="hl-btn-outline px-4 py-2 text-xs font-medium cursor-pointer"
>
取消 取消
</button> </Button>
)} )}
<button <Button type="submit" disabled={loading} className="px-4">
type="submit"
disabled={loading}
className="hl-btn-primary px-5 py-2 text-xs font-semibold flex items-center gap-2 cursor-pointer"
>
<Terminal className="w-3.5 h-3.5" /> <Terminal className="w-3.5 h-3.5" />
<span>{loading ? '正在验证签名...' : '解锁并进入工作台'}</span> <span>{loading ? '正在验证签名...' : '解锁并进入工作台'}</span>
</button> </Button>
</div> </div>
</form> </form>
</div> </Card>
); );
}; };
@@ -1,6 +1,14 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useMemo } from 'react';
import { Search } from 'lucide-react';
import { usePortalStore } from '../store/usePortalStore'; import { usePortalStore } from '../store/usePortalStore';
import { Badge } from './ui/badge';
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from './ui/command';
interface CmdItem { interface CmdItem {
id: string; id: string;
@@ -21,16 +29,6 @@ export const CommandPalette: React.FC = () => {
getEffectiveMode, getEffectiveMode,
} = usePortalStore(); } = usePortalStore();
const [query, setQuery] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (showCmdPalette) {
setQuery('');
setTimeout(() => inputRef.current?.focus(), 20);
}
}, [showCmdPalette]);
const commands = useMemo<CmdItem[]>(() => { const commands = useMemo<CmdItem[]>(() => {
const mode = getEffectiveMode(); const mode = getEffectiveMode();
const resolve = (edge: string, home: string, lan: string) => const resolve = (edge: string, home: string, lan: string) =>
@@ -156,86 +154,32 @@ export const CommandPalette: React.FC = () => {
]; ];
}, [getEffectiveMode, setShowAgyModal, setTheme, triggerAction, copyCmd]); }, [getEffectiveMode, setShowAgyModal, setTheme, triggerAction, copyCmd]);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return commands;
return commands.filter(
(c) => c.title.toLowerCase().includes(q) || c.hint.toLowerCase().includes(q)
);
}, [query, commands]);
if (!showCmdPalette) return null;
const execute = (cmd: CmdItem) => {
setShowCmdPalette(false);
cmd.run();
};
return ( return (
<div <CommandDialog open={showCmdPalette} onOpenChange={setShowCmdPalette}>
onClick={(e) => { <CommandInput placeholder="输入服务名称、SSH 指令、路由模式或主题..." />
if (e.target === e.currentTarget) setShowCmdPalette(false); <CommandList>
}} <CommandEmpty>未找到匹配的指令或服务</CommandEmpty>
className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-start justify-center pt-20 px-4" <CommandGroup heading="快速操作与服务直达 (cmdk + Radix Dialog)">
> {commands.map((cmd) => (
<div className="w-full max-w-xl hl-card overflow-hidden shadow-2xl"> <CommandItem
<div className="px-4 py-3 border-b hl-border flex items-center gap-3">
<Search className="w-4 h-4" style={{ color: 'var(--text-muted)' }} />
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
type="text"
placeholder="输入服务名称、SSH 指令、路由模式或主题..."
className="w-full bg-transparent text-sm outline-none"
onKeyDown={(e) => {
if (e.key === 'Escape') setShowCmdPalette(false);
if (e.key === 'Enter' && filtered.length > 0) execute(filtered[0]);
}}
/>
<kbd
className="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted"
style={{ color: 'var(--text-muted)' }}
>
ESC
</kbd>
</div>
<div className="max-h-80 overflow-y-auto p-2 text-xs">
<div
className="px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider"
style={{ color: 'var(--text-subtle)' }}
>
快速操作与服务直达
</div>
{filtered.map((cmd) => (
<button
key={cmd.id} key={cmd.id}
onClick={() => execute(cmd)} value={`${cmd.title} ${cmd.hint} ${cmd.type}`}
className="w-full px-3 py-2 rounded-lg text-left flex items-center justify-between transition-colors cursor-pointer" onSelect={() => {
onMouseEnter={(e) => { setShowCmdPalette(false);
e.currentTarget.style.backgroundColor = 'var(--bg-muted)'; cmd.run();
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
}} }}
> >
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<span className="font-medium">{cmd.title}</span> <span className="font-medium">{cmd.title}</span>
<span className="font-mono-num text-[11px]" style={{ color: 'var(--text-muted)' }}> <span className="font-mono-num text-[11px] text-[var(--text-muted)]">
{cmd.hint} {cmd.hint}
</span> </span>
</div> </div>
<span <Badge variant="secondary">{cmd.type}</Badge>
className="text-[10px] font-mono-num px-1.5 py-0.5 rounded hl-muted" </CommandItem>
style={{ color: 'var(--text-muted)' }}
>
{cmd.type}
</span>
</button>
))} ))}
</div> </CommandGroup>
</div> </CommandList>
</div> </CommandDialog>
); );
}; };
@@ -15,6 +15,12 @@ import { usePortalStore } from '../store/usePortalStore';
import { AuthGateCard } from './AuthGateModal'; import { AuthGateCard } from './AuthGateModal';
import { ServiceCard } from './ServiceCard'; import { ServiceCard } from './ServiceCard';
import { SshShelf } from './SshShelf'; import { SshShelf } from './SshShelf';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Card } from './ui/card';
import { Input, InputGroup } from './ui/input';
import { Switch } from './ui/switch';
import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
type StoryId = type StoryId =
| 'layout-architecture' | 'layout-architecture'
@@ -32,6 +38,7 @@ export const StorybookWorkbench: React.FC = () => {
setTheme, setTheme,
setLayoutPreset, setLayoutPreset,
setViewMode, setViewMode,
setShowCmdPalette,
telemetry, telemetry,
showToast, showToast,
} = usePortalStore(); } = usePortalStore();
@@ -43,6 +50,7 @@ export const StorybookWorkbench: React.FC = () => {
// Interactive Playground Props for Form Inputs story // Interactive Playground Props for Form Inputs story
const [demoSearch, setDemoSearch] = useState(''); const [demoSearch, setDemoSearch] = useState('');
const [demoHost, setDemoHost] = useState('192.168.31.164'); const [demoHost, setDemoHost] = useState('192.168.31.164');
const [demoSwitch, setDemoSwitch] = useState(true);
const stories: Array<{ id: StoryId; group: string; title: string; icon: React.ReactNode }> = [ const stories: Array<{ id: StoryId; group: string; title: string; icon: React.ReactNode }> = [
{ {
@@ -59,19 +67,19 @@ export const StorybookWorkbench: React.FC = () => {
}, },
{ {
id: 'form-inputs', id: 'form-inputs',
group: 'COMPONENTS / 核心组件', group: 'SHADCN / RADIX UI 组件',
title: '03. Input & AuthGate 输入框与登录弹窗', title: '03. shadcn/ui + Radix 原语与认证弹窗',
icon: <KeyRound className="w-3.5 h-3.5" />, icon: <KeyRound className="w-3.5 h-3.5" />,
}, },
{ {
id: 'service-card', id: 'service-card',
group: 'COMPONENTS / 核心组件', group: 'SHADCN / RADIX UI 组件',
title: '04. ServiceCard 内联遥测卡片', title: '04. ServiceCard 内联遥测卡片',
icon: <Layers className="w-3.5 h-3.5" />, icon: <Layers className="w-3.5 h-3.5" />,
}, },
{ {
id: 'ssh-shelf', id: 'ssh-shelf',
group: 'COMPONENTS / 核心组件', group: 'SHADCN / RADIX UI 组件',
title: '05. SshShelf 快捷指令栏', title: '05. SshShelf 快捷指令栏',
icon: <Terminal className="w-3.5 h-3.5" />, icon: <Terminal className="w-3.5 h-3.5" />,
}, },
@@ -81,13 +89,13 @@ export const StorybookWorkbench: React.FC = () => {
<div className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6"> <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"> <div className="grid grid-cols-1 lg:grid-cols-4 gap-6 items-start">
{/* Left Sidebar: Storybook Navigation Tree */} {/* Left Sidebar: Storybook Navigation Tree */}
<aside className="hl-card p-4 space-y-4 lg:sticky lg:top-20"> <Card className="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 justify-between border-b hl-border pb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="w-4 h-4" style={{ color: 'var(--accent)' }} /> <BookOpen className="w-4 h-4" style={{ color: 'var(--accent)' }} />
<span className="text-xs font-bold uppercase tracking-wider">Storybook 组件库</span> <span className="text-xs font-bold uppercase tracking-wider">shadcn/ui 组件库</span>
</div> </div>
<span className="text-[10px] font-mono-num px-1.5 py-0.5 rounded hl-muted">v3.2</span> <Badge variant="accent">Radix + cmdk</Badge>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -113,39 +121,27 @@ export const StorybookWorkbench: React.FC = () => {
<div className="pt-3 border-t hl-border space-y-2 text-xs"> <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 className="text-[11px] font-semibold" style={{ color: 'var(--text-muted)' }}>
全局外观状态预览 全局外观状态预览 (@radix-ui/react-tabs)
</div> </div>
<div className="grid grid-cols-3 gap-1 p-1 rounded-lg hl-muted border hl-border"> <Tabs value={theme} onValueChange={(v) => setTheme(v as 'system' | 'light' | 'dark')}>
{(['system', 'light', 'dark'] as const).map((t) => ( <TabsList className="w-full grid grid-cols-3">
<button <TabsTrigger value="system">自动</TabsTrigger>
key={t} <TabsTrigger value="light">浅色</TabsTrigger>
onClick={() => setTheme(t)} <TabsTrigger value="dark">深色</TabsTrigger>
className="py-1 rounded text-[11px] font-medium cursor-pointer" </TabsList>
style={ </Tabs>
theme === t <Button onClick={() => setViewMode('dashboard')} className="w-full mt-2">
? { 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> </Button>
</div> </div>
</aside> </Card>
{/* Right Canvas: Interactive Story Preview & Controls */} {/* Right Canvas: Interactive Story Preview & Controls */}
<div className="lg:col-span-3 space-y-6"> <div className="lg:col-span-3 space-y-6">
{/* STORY 1: Layout Architecture */} {/* STORY 1: Layout Architecture */}
{activeStory === 'layout-architecture' && ( {activeStory === 'layout-architecture' && (
<div className="space-y-6"> <div className="space-y-6">
<div className="hl-card p-6 space-y-4"> <Card className="p-6 space-y-4">
<div className="flex items-center justify-between border-b hl-border pb-4"> <div className="flex items-center justify-between border-b hl-border pb-4">
<div> <div>
<span <span
@@ -168,7 +164,7 @@ export const StorybookWorkbench: React.FC = () => {
{/* Visual Blueprint Diagram of Two-Tier Header */} {/* 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-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"> <div className="p-3 rounded-lg hl-card flex items-center justify-between">
<span>[左] 🍎 HomeLab · 导航切换 (中控台 / Storybook)</span> <span>[左] 🍎 HomeLab · Radix Tabs 切换 (中控台 / Storybook)</span>
<span style={{ color: 'var(--accent)' }}> <span style={{ color: 'var(--accent)' }}>
[最右固定] 🔍 搜索 ⌘K · &gt;_ AGY 终端 · ☀️/🌙 主题切换 [最右固定] 🔍 搜索 ⌘K · &gt;_ AGY 终端 · ☀️/🌙 主题切换
</span> </span>
@@ -177,7 +173,7 @@ export const StorybookWorkbench: React.FC = () => {
<span style={{ color: 'var(--success)' }}> <span style={{ color: 'var(--success)' }}>
[左] 实时硬件遥测条 (M6 CPU · RAM · Z4 20TB · SG 专线) [左] 实时硬件遥测条 (M6 CPU · RAM · Z4 20TB · SG 专线)
</span> </span>
<span>[右对齐] 路由模式切换器 (智能路由 / 443 / 10443 / LAN)</span> <span>[右对齐] Radix Tabs 路由切换器 (智能路由 / 443 / 10443 / LAN)</span>
</div> </div>
</div> </div>
@@ -225,13 +221,13 @@ export const StorybookWorkbench: React.FC = () => {
})} })}
</div> </div>
</div> </div>
</div> </Card>
</div> </div>
)} )}
{/* STORY 2: Theme Tokens */} {/* STORY 2: Theme Tokens */}
{activeStory === 'theme-tokens' && ( {activeStory === 'theme-tokens' && (
<div className="hl-card p-6 space-y-6"> <Card className="p-6 space-y-6">
<div className="border-b hl-border pb-4 flex items-center justify-between"> <div className="border-b hl-border pb-4 flex items-center justify-between">
<div> <div>
<span <span
@@ -244,9 +240,9 @@ export const StorybookWorkbench: React.FC = () => {
主流双主题色板规范(当前生效:{resolvedTheme === 'dark' ? '深色极客' : '浅色纯净'}) 主流双主题色板规范(当前生效:{resolvedTheme === 'dark' ? '深色极客' : '浅色纯净'})
</h2> </h2>
</div> </div>
<span className="text-xs font-mono-num px-2.5 py-1 rounded hl-muted"> <Badge variant="secondary">
模式: {theme === 'system' ? '跟随设备自动切换' : '手动锁定'} 模式: {theme === 'system' ? '跟随设备自动切换' : '手动锁定'}
</span> </Badge>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
@@ -319,39 +315,75 @@ export const StorybookWorkbench: React.FC = () => {
); );
})} })}
</div> </div>
</div> </Card>
)} )}
{/* STORY 3: Custom Form Inputs & AuthGate Card */} {/* STORY 3: Official shadcn/ui + Radix UI Primitives & AuthGate Card */}
{activeStory === 'form-inputs' && ( {activeStory === 'form-inputs' && (
<div className="hl-card p-6 space-y-6"> <Card className="p-6 space-y-6">
<div className="border-b hl-border pb-4 flex items-center justify-between"> <div className="border-b hl-border pb-4 flex items-center justify-between">
<div> <div>
<span <span
className="text-[11px] font-mono-num uppercase tracking-wider" className="text-[11px] font-mono-num uppercase tracking-wider"
style={{ color: 'var(--accent)' }} style={{ color: 'var(--accent)' }}
> >
Components / Form Controls & SSO Auth Gate src/components/ui/* · Official shadcn/ui + Radix Primitives + cmdk
</span> </span>
<h2 className="text-lg font-bold mt-0.5"> <h2 className="text-lg font-bold mt-0.5">
统一自定义输入框 (`.hl-input`) 与 SSO 认证卡片(彻底禁用浏览器原生弹窗) 开源 `shadcn/ui` 组件库(`Input` / `Button` / `Switch` / `Badge` / `CommandDialog` / `AuthGateCard`)
</h2> </h2>
</div> </div>
<KeyRound className="w-4 h-4" style={{ color: 'var(--accent)' }} /> <KeyRound className="w-4 h-4" style={{ color: 'var(--accent)' }} />
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
{/* Left: Input Primitives Showcase */} {/* Left: shadcn/ui Primitives Showcase */}
<div className="p-5 rounded-xl hl-muted border hl-border space-y-4"> <div className="p-5 rounded-xl hl-muted border hl-border space-y-4">
<div className="text-xs font-bold uppercase tracking-wider" style={{ color: 'var(--text-muted)' }}> <div
1. 基础表单输入组件规范 (`.hl-input` / `.hl-input-group`) className="text-xs font-bold uppercase tracking-wider"
style={{ color: 'var(--text-muted)' }}
>
1. `src/components/ui/*` 标准原语展示
</div> </div>
{/* Button Variants */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="block text-xs font-medium" style={{ color: 'var(--text-muted)' }}> <label className="block text-xs font-medium" style={{ color: 'var(--text-muted)' }}>
带前缀图标与快捷键提示的搜索框 Button 变体 (`cva` + `@radix-ui/react-slot`)
</label> </label>
<div className="hl-input-group"> <div className="flex flex-wrap items-center gap-2">
<Button variant="default">Primary 主按钮</Button>
<Button variant="outline">Outline 描边</Button>
<Button variant="secondary">Secondary 次级</Button>
<Button variant="accent">Accent 高亮</Button>
<Button variant="ghost" onClick={() => setShowCmdPalette(true)}>
唤起 cmdk 面板 ⌘K
</Button>
</div>
</div>
{/* Badge Variants & Radix Switch */}
<div className="space-y-1.5">
<label className="block text-xs font-medium" style={{ color: 'var(--text-muted)' }}>
Badge 标签与 `@radix-ui/react-switch` 开关
</label>
<div className="flex flex-wrap items-center justify-between gap-2 p-2.5 rounded-lg hl-card">
<div className="flex items-center gap-1.5">
<Badge variant="default">Default</Badge>
<Badge variant="accent">Radix UI</Badge>
<Badge variant="success">Online</Badge>
<Badge variant="outline">v3.2</Badge>
</div>
<Switch checked={demoSwitch} onCheckedChange={setDemoSwitch} />
</div>
</div>
{/* Input & InputGroup */}
<div className="space-y-1.5">
<label className="block text-xs font-medium" style={{ color: 'var(--text-muted)' }}>
`InputGroup` 组合输入框
</label>
<InputGroup>
<Search className="w-4 h-4 shrink-0" style={{ color: 'var(--text-subtle)' }} /> <Search className="w-4 h-4 shrink-0" style={{ color: 'var(--text-subtle)' }} />
<input <input
type="text" type="text"
@@ -365,35 +397,21 @@ export const StorybookWorkbench: React.FC = () => {
> >
⌘K ⌘K
</kbd> </kbd>
</div> </InputGroup>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="block text-xs font-medium" style={{ color: 'var(--text-muted)' }}> <label className="block text-xs font-medium" style={{ color: 'var(--text-muted)' }}>
等宽字体 IP / 域名输入框 (`.hl-input .font-mono-num`) `Input` 等宽字体输入框
</label> </label>
<input <Input
type="text" type="text"
value={demoHost} value={demoHost}
onChange={(e) => setDemoHost(e.target.value)} onChange={(e) => setDemoHost(e.target.value)}
className="hl-input font-mono-num" className="font-mono-num"
placeholder="192.168.31.164" placeholder="192.168.31.164"
/> />
</div> </div>
<div className="p-3 rounded-lg hl-card text-[11px] leading-relaxed space-y-1" style={{ color: 'var(--text-muted)' }}>
<div className="font-semibold" style={{ color: 'var(--text-main)' }}>
为什么不再出现浏览器默认输入框和弹窗?
</div>
<p>
1. 全局 CSS 已设置 <code>appearance: none</code> 与自定义聚焦光环{' '}
<code>box-shadow: 0 0 0 3px var(--accent-soft)</code>,并覆盖了 Chrome 自动填充的黄色背景。
</p>
<p>
2. Caddy 网关已移除 <code>basicauth</code>,改用 <code>forward_auth</code> 对接右侧的{' '}
<strong>HomeLab SSO 2.0 统一认证卡片</strong>,一次登录全域名 30 天免密通行。
</p>
</div>
</div> </div>
{/* Right: Live AuthGateCard Preview */} {/* Right: Live AuthGateCard Preview */}
@@ -405,12 +423,12 @@ export const StorybookWorkbench: React.FC = () => {
/> />
</div> </div>
</div> </div>
</div> </Card>
)} )}
{/* STORY 4: ServiceCard Interactive Playground */} {/* STORY 4: ServiceCard Interactive Playground */}
{activeStory === 'service-card' && ( {activeStory === 'service-card' && (
<div className="hl-card p-6 space-y-6"> <Card className="p-6 space-y-6">
<div className="border-b hl-border pb-4 flex items-center justify-between"> <div className="border-b hl-border pb-4 flex items-center justify-between">
<div> <div>
<span <span
@@ -428,27 +446,24 @@ export const StorybookWorkbench: React.FC = () => {
{/* Interactive Controls */} {/* Interactive Controls */}
<div className="p-4 rounded-xl hl-muted border hl-border flex flex-wrap items-center gap-6 text-xs"> <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"> <div className="flex items-center gap-2.5">
<span className="font-semibold">在线状态 (online):</span> <span className="font-semibold">在线状态 (online):</span>
<button <Switch checked={demoOnline} onCheckedChange={setDemoOnline} />
onClick={() => setDemoOnline(!demoOnline)} <span className="font-mono-num">{demoOnline ? 'true' : 'false'}</span>
className="px-2.5 py-1 rounded hl-card font-mono-num cursor-pointer"
>
{demoOnline ? 'true (绿灯)' : 'false (离线红灯)'}
</button>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-semibold">右上角动作按钮 (actionType):</span> <span className="font-semibold">右上角动作按钮 (actionType):</span>
{(['drawer', 'backup', undefined] as const).map((act, i) => ( {(['drawer', 'backup', undefined] as const).map((act, i) => (
<button <Button
key={i} key={i}
variant={demoAction === act ? 'accent' : 'outline'}
size="sm"
onClick={() => setDemoAction(act)} onClick={() => setDemoAction(act)}
className="px-2.5 py-1 rounded hl-card font-mono-num cursor-pointer" className="font-mono-num"
style={demoAction === act ? { borderColor: 'var(--accent)' } : {}}
> >
{act || 'route-badge'} {act || 'route-badge'}
</button> </Button>
))} ))}
</div> </div>
</div> </div>
@@ -476,12 +491,12 @@ export const StorybookWorkbench: React.FC = () => {
}} }}
/> />
</div> </div>
</div> </Card>
)} )}
{/* STORY 5: SshShelf */} {/* STORY 5: SshShelf */}
{activeStory === 'ssh-shelf' && ( {activeStory === 'ssh-shelf' && (
<div className="hl-card p-6 space-y-6"> <Card className="p-6 space-y-6">
<div className="border-b hl-border pb-4"> <div className="border-b hl-border pb-4">
<span <span
className="text-[11px] font-mono-num uppercase tracking-wider" className="text-[11px] font-mono-num uppercase tracking-wider"
@@ -492,7 +507,7 @@ export const StorybookWorkbench: React.FC = () => {
<h2 className="text-lg font-bold mt-0.5">底部 SSH 快捷复制与运维操作栏</h2> <h2 className="text-lg font-bold mt-0.5">底部 SSH 快捷复制与运维操作栏</h2>
</div> </div>
<SshShelf /> <SshShelf />
</div> </Card>
)} )}
</div> </div>
</div> </div>
+42 -67
View File
@@ -2,6 +2,8 @@ import React from 'react';
import { BookOpen, LayoutGrid, 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';
import { Button } from './ui/button';
import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
const MODES: Array<{ id: RouteModeId; label: string }> = [ const MODES: Array<{ id: RouteModeId; label: string }> = [
{ id: 'auto', label: '智能路由' }, { id: 'auto', label: '智能路由' },
@@ -29,15 +31,15 @@ export const TopRibbon: React.FC = () => {
<> <>
{/* ===================================================================== {/* =====================================================================
Tier 1: Standard AppShell Global Top Navbar (h-14, NEVER wraps!) Tier 1: Standard AppShell Global Top Navbar (h-14, NEVER wraps!)
Left: Brand + View Tabs (中控台 / Storybook) Left: Brand + Radix Tabs (中控台 / Storybook)
Far Right (Red Box): Search (Cmd+K) + AGY Terminal + Theme Switcher Far Right: 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-card) 88%, 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"> <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 */} {/* Left Group: Brand Logo + Official Radix Navigation Tabs */}
<div className="flex items-center gap-4 shrink-0"> <div className="flex items-center gap-4 shrink-0">
<div <div
onClick={() => setViewMode('dashboard')} onClick={() => setViewMode('dashboard')}
@@ -58,47 +60,30 @@ export const TopRibbon: React.FC = () => {
<div className="h-4 w-[1px] hl-muted" /> <div className="h-4 w-[1px] hl-muted" />
{/* View Switcher Tabs: Dashboard vs Storybook */} {/* View Switcher Tabs via @radix-ui/react-tabs */}
<nav className="flex items-center gap-1 text-xs"> <Tabs
<button value={viewMode}
onClick={() => setViewMode('dashboard')} onValueChange={(v) => setViewMode(v as 'dashboard' | 'storybook')}
className="px-2.5 py-1.5 rounded-md font-medium flex items-center gap-1.5 transition-all cursor-pointer" >
style={ <TabsList>
viewMode === 'dashboard' <TabsTrigger value="dashboard">
? { <LayoutGrid className="w-3.5 h-3.5" />
backgroundColor: 'var(--bg-muted)', <span>中控台</span>
color: 'var(--text-main)', </TabsTrigger>
} <TabsTrigger value="storybook">
: { color: 'var(--text-muted)' } <BookOpen className="w-3.5 h-3.5" />
} <span>Storybook 设计规范</span>
> </TabsTrigger>
<LayoutGrid className="w-3.5 h-3.5" /> </TabsList>
<span>中控台</span> </Tabs>
</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>
{/* Far Right Group: Pinned to Right Edge (Search + AGY + Theme Switcher) */} {/* Far Right Group: Pinned to Right Edge (Search + AGY + Theme Switcher) */}
<div className="flex items-center gap-2.5 shrink-0 ml-auto"> <div className="flex items-center gap-2.5 shrink-0 ml-auto">
<button <Button
variant="outline"
onClick={() => setShowCmdPalette(true)} onClick={() => setShowCmdPalette(true)}
className="hl-btn-outline px-3 py-1.5 text-xs flex items-center gap-3 cursor-pointer" className="gap-3"
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)' }}>
@@ -111,15 +96,12 @@ export const TopRibbon: React.FC = () => {
> >
⌘K ⌘K
</kbd> </kbd>
</button> </Button>
<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"
>
<Terminal className="w-3.5 h-3.5" /> <Terminal className="w-3.5 h-3.5" />
<span>AGY 终端</span> <span>AGY 终端</span>
</button> </Button>
{/* Top-Right Theme Toggle (Pinned at Far Right Corner!) */} {/* 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">
@@ -163,7 +145,7 @@ export const TopRibbon: React.FC = () => {
{/* ===================================================================== {/* =====================================================================
Tier 2: Subheader Telemetry & Route Mode Bar (Aligned with Grid) Tier 2: Subheader Telemetry & Route Mode Bar (Aligned with Grid)
Left: 4 Hardware & Network Gauges Left: 4 Hardware & Network Gauges
Right: Route Mode Segmented Switcher (智能路由 / 443 / 10443 / LAN) Right: Route Mode Segmented Switcher via @radix-ui/react-tabs
===================================================================== */} ===================================================================== */}
{viewMode === 'dashboard' && ( {viewMode === 'dashboard' && (
<div className="border-b hl-border" style={{ backgroundColor: 'var(--bg-card)' }}> <div className="border-b hl-border" style={{ backgroundColor: 'var(--bg-card)' }}>
@@ -217,27 +199,20 @@ export const TopRibbon: React.FC = () => {
</div> </div>
</div> </div>
{/* Right: Route Mode Switcher (Pinned to Right Edge of Subheader) */} {/* Right: Route Mode Switcher via @radix-ui/react-tabs */}
<div className="flex items-center p-0.5 rounded-lg hl-muted border hl-border text-xs ml-auto"> <Tabs
{MODES.map((m) => ( value={routeMode}
<button onValueChange={(v) => setRouteMode(v as RouteModeId)}
key={m.id} className="ml-auto"
onClick={() => setRouteMode(m.id)} >
className="px-2.5 py-1 rounded-md font-medium transition-all cursor-pointer" <TabsList>
style={ {MODES.map((m) => (
routeMode === m.id <TabsTrigger key={m.id} value={m.id}>
? { {m.label}
backgroundColor: 'var(--bg-card)', </TabsTrigger>
color: 'var(--text-main)', ))}
boxShadow: '0 1px 2px rgba(0,0,0,0.08)', </TabsList>
} </Tabs>
: { color: 'var(--text-muted)' }
}
>
{m.label}
</button>
))}
</div>
</div> </div>
</div> </div>
)} )}
@@ -0,0 +1,31 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const badgeVariants = cva(
'inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] font-mono-num font-medium transition-colors shrink-0',
{
variants: {
variant: {
default: 'bg-[var(--primary)] text-[var(--primary-fg)]',
secondary: 'hl-muted text-[var(--text-muted)] border hl-border',
outline: 'border hl-border text-[var(--text-muted)] bg-[var(--bg-card)]',
accent: 'bg-[var(--accent-soft)] text-[var(--accent)] font-semibold',
success: 'bg-[var(--success-soft)] text-[var(--success)] font-semibold',
},
},
defaultVariants: {
variant: 'secondary',
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
@@ -0,0 +1,52 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const buttonVariants = cva(
'inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-lg text-xs font-medium transition-all cursor-pointer select-none disabled:pointer-events-none disabled:opacity-50 shrink-0',
{
variants: {
variant: {
default: 'hl-btn-primary shadow-xs',
outline: 'hl-btn-outline',
secondary: 'hl-muted border hl-border hover:opacity-90',
ghost: 'hover:bg-[var(--bg-muted)]',
accent:
'border border-[var(--accent)] bg-[var(--accent-soft)] text-[var(--accent)] font-semibold hover:opacity-90',
},
size: {
default: 'h-8 px-3 py-1.5',
sm: 'h-7 px-2.5 text-[11px] rounded-md',
lg: 'h-9 px-4 text-xs',
icon: 'h-8 w-8 p-0',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };
@@ -0,0 +1,55 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('hl-card', className)} {...props} />
)
);
Card.displayName = 'Card';
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-5', className)} {...props} />
)
);
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('text-sm font-bold leading-none tracking-tight', className)}
{...props}
/>
)
);
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-xs text-[var(--text-muted)]', className)}
{...props}
/>
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-5 pt-0', className)} {...props} />
)
);
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-5 pt-0', className)} {...props} />
)
);
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,121 @@
import * as React from 'react';
import { Command as CommandPrimitive } from 'cmdk';
import { Search } from 'lucide-react';
import { cn } from '../../lib/utils';
import { Dialog, DialogContent, DialogDescription, DialogTitle } from './dialog';
const Command = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
'flex h-full w-full flex-col overflow-hidden rounded-xl bg-[var(--bg-card)] text-[var(--text-main)]',
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> {}
const CommandDialog: React.FC<CommandDialogProps> = ({ children, ...props }) => {
return (
<Dialog {...props}>
<DialogContent className="max-w-xl overflow-hidden p-0 shadow-2xl top-[22%] translate-y-0">
<DialogTitle className="sr-only">全局命令与服务搜索面板</DialogTitle>
<DialogDescription className="sr-only">
使用键盘上下方向键选择服务或操作,回车立即执行
</DialogDescription>
<Command>{children}</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b hl-border px-4 py-3 gap-3" cmdk-input-wrapper="">
<Search className="h-4 w-4 shrink-0 text-[var(--text-muted)]" />
<CommandPrimitive.Input
ref={ref}
className={cn(
'flex w-full bg-transparent text-sm outline-none placeholder:text-[var(--text-subtle)] disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
/>
<kbd className="px-1.5 py-0.5 text-[10px] font-mono-num rounded hl-muted text-[var(--text-muted)] shrink-0">
ESC
</kbd>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn('max-h-80 overflow-y-auto overflow-x-hidden p-2', className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-xs text-[var(--text-muted)]"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
'overflow-hidden p-1 text-[var(--text-main)] [&_[cmdk-group-heading]]:px-2.5 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-[10px] [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-wider [&_[cmdk-group-heading]]:text-[var(--text-subtle)]',
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandItem = React.forwardRef<
React.ComponentRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-pointer select-none items-center justify-between rounded-lg px-3 py-2 text-xs outline-none transition-colors data-[selected=true]:bg-[var(--bg-muted)] data-[selected=true]:text-[var(--text-main)] data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
};
@@ -0,0 +1,90 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { cn } from '../../lib/utils';
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
'fixed inset-0 z-50 bg-black/70 backdrop-blur-sm transition-opacity',
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] hl-card shadow-2xl duration-200 outline-none',
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col space-y-1.5 text-left', className)}
{...props}
/>
);
DialogHeader.displayName = 'DialogHeader';
const DialogTitle = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-sm font-bold leading-none tracking-tight', className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-xs text-[var(--text-muted)]', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
};
@@ -0,0 +1,29 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn('hl-input', className)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = 'Input';
export interface InputGroupProps extends React.HTMLAttributes<HTMLDivElement> {}
const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
({ className, ...props }, ref) => {
return <div ref={ref} className={cn('hl-input-group', className)} {...props} />;
}
);
InputGroup.displayName = 'InputGroup';
export { Input, InputGroup };
@@ -0,0 +1,26 @@
import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '../../lib/utils';
const Switch = React.forwardRef<
React.ComponentRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border transition-colors disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-[var(--accent)] data-[state=checked]:border-[var(--accent)] data-[state=unchecked]:bg-[var(--bg-muted)] data-[state=unchecked]:border-[var(--border-color)]',
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
'pointer-events-none block h-3.5 w-3.5 rounded-full bg-white shadow-sm transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0.5'
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
@@ -0,0 +1,49 @@
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '../../lib/utils';
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-lg hl-muted p-1 border hl-border text-[var(--text-muted)]',
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center gap-1.5 whitespace-nowrap rounded-md px-2.5 py-1 text-xs font-medium transition-all cursor-pointer select-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-[var(--bg-card)] data-[state=active]:text-[var(--text-main)] data-[state=active]:shadow-xs',
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn('mt-2 outline-none', className)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };