chore: wave-0 baseline (Tauri2+React shell, platform detect, fake catalog)

This commit is contained in:
AgentDock 施工员
2026-08-25 00:21:13 +08:00
commit 296843215f
137 changed files with 6696 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AgentDock</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+2137
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "agentdock-desktop",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "^2",
"lucide-react": "^0.525.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@vitejs/plugin-react": "^4.5.0",
"typescript": "^5.6.3",
"vite": "^6.3.5"
}
}
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "agentdock-desktop"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "AgentDock 桌面壳(Tauri 2"
[lib]
name = "agentdock_desktop_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
agentdock-platform = { path = "../../../crates/agentdock-platform" }
agentdock-adapter = { path = "../../../crates/agentdock-adapter" }
agentdock-store = { path = "../../../crates/agentdock-store" }
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "默认能力:主窗口(Wave 0 仅调用自有 invoke 命令)",
"windows": ["main"],
"permissions": ["core:default"]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,11 @@
//! IPC 命令:假目录(架构 §2 adapters/IPC 契约 listCatalog
use agentdock_adapter::catalog::{self, CatalogEntry};
use crate::adapters_dir;
/// 读取 adapters/catalog.yaml 与 tools/*.yaml,返回 14 个目录条目
#[tauri::command(rename = "listCatalog")]
pub fn list_catalog() -> Result<Vec<CatalogEntry>, String> {
catalog::load_catalog(adapters_dir()).map_err(|e| e.to_string())
}
@@ -0,0 +1,10 @@
//! IPC 命令:本机环境检测(架构 §5,IPC 契约 detectEnv
use agentdock_platform::detect;
use agentdock_platform::model::PlatformEnv;
/// 返回本机真实环境快照(Windows/Linux 双平台)
#[tauri::command(rename = "detectEnv")]
pub fn detect_env() -> PlatformEnv {
detect::detect_env()
}
@@ -0,0 +1,2 @@
pub mod catalog;
pub mod env;
+42
View File
@@ -0,0 +1,42 @@
mod commands;
use std::path::PathBuf;
/// 解析适配器目录。
/// 优先级:环境变量 AGENTDOCK_ADAPTERS_DIR > CWD 相对路径(tauri dev 时 CWD=apps/desktop
/// > CARGO_MANIFEST_DIR 相对路径(仓库根)。
fn adapters_dir() -> PathBuf {
if let Ok(dir) = std::env::var("AGENTDOCK_ADAPTERS_DIR") {
return PathBuf::from(dir);
}
let cwd = std::env::current_dir().unwrap_or_default();
let cargo_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let candidates = [
cwd.join("../../adapters"),
cargo_dir.join("../../../adapters"),
];
for c in candidates {
if c.join("catalog.yaml").exists() {
return c;
}
}
cargo_dir.join("../../../adapters")
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|_app| {
// 自测标记:`tauri dev` 启动后日志出现该行即代表壳已就绪
println!("[agentdock] window-ready");
// Wave 0:初始化 SQLite 空库(架构 §2 状态存储层)
let _ = agentdock_store::Store::open_default();
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::env::detect_env,
commands::catalog::list_catalog,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
agentdock_desktop_lib::run()
}
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "AgentDock",
"version": "0.1.0",
"identifier": "cn.agentdock.desktop",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "AgentDock",
"width": 1280,
"height": 800,
"minWidth": 960,
"minHeight": 600,
"center": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": ["nsis"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.ico"
]
}
}
+65
View File
@@ -0,0 +1,65 @@
import { useMemo, useState } from "react";
import { Sidebar, type PageKey, PAGE_META } from "./components/Sidebar";
import { Header } from "./components/Header";
import { OverviewPage } from "./pages/Overview";
import { CatalogPage } from "./pages/Catalog";
import { MyCliPage } from "./pages/MyCli";
import { ConfigCenterPage } from "./pages/ConfigCenter";
import { BackupPage } from "./pages/Backup";
import { SettingsPage } from "./pages/Settings";
import { useEnv } from "./hooks/useEnv";
import type { PlatformEnv, RuntimeInfo } from "./ipc/types";
/** 平台相关运行时集合(Windows 不看 aptLinux 不看 winget */
const WIN_RUNTIMES = ["node", "npm", "python", "uv", "git", "winget"] as const;
const LINUX_RUNTIMES = ["node", "npm", "python", "uv", "git", "apt"] as const;
function envWarningCount(env: PlatformEnv | null): number {
if (!env) return 0;
const keys = env.os === "windows" ? WIN_RUNTIMES : LINUX_RUNTIMES;
let n = 0;
for (const k of keys) {
const info = env.runtimes[k] as RuntimeInfo | null | undefined;
if (info && info.status !== "installed") n++;
}
return n;
}
export default function App() {
const [page, setPage] = useState<PageKey>("overview");
const { env } = useEnv();
const warningCount = envWarningCount(env);
const content = useMemo(() => {
switch (page) {
case "overview":
return <OverviewPage />;
case "catalog":
return <CatalogPage />;
case "my-cli":
return <MyCliPage />;
case "config":
return <ConfigCenterPage />;
case "backup":
return <BackupPage />;
case "settings":
return <SettingsPage />;
}
}, [page]);
return (
<div className="app-shell">
<div className="bg-grid grid-drift" aria-hidden="true" />
<Sidebar current={page} onNavigate={setPage} />
<div className="app-main">
<Header title={PAGE_META[page].title} warningCount={warningCount} />
<main className="app-content">
{/* key=page 触发 §4.2 页面切换动效:translateY(8px)+淡入 */}
<div key={page} className="page-enter">
{content}
</div>
</main>
</div>
</div>
);
}
@@ -0,0 +1,19 @@
/** CLI 单色字母头像(视觉规范 §7:双字母单色,--ad-bg-2 底 + 1px --ad-border */
export function CliMonogram({
name,
size = 32,
}: {
name: string;
size?: number;
}) {
const chars = name.replace(/\s+/g, "").slice(0, 2).toUpperCase();
return (
<span
className="monogram"
style={{ width: size, height: size, fontSize: Math.round(size * 0.42) }}
aria-hidden="true"
>
{chars}
</span>
);
}
+34
View File
@@ -0,0 +1,34 @@
import { ChevronDown, Search } from "lucide-react";
interface HeaderProps {
title: string;
/** 环境警告条数(0 时整颗 chip 不渲染,规范 §3.0) */
warningCount?: number;
}
/** 顶栏(视觉规范 §3.0):页面标题 + 搜索框 + 环境警告 chip */
export function Header({ title, warningCount = 0 }: HeaderProps) {
return (
<header className="header">
<h1 className="header-title">{title}</h1>
<div className="header-actions">
<div className="search-box">
<Search size={16} strokeWidth={1.5} className="search-icon" aria-hidden="true" />
<input
type="search"
placeholder="搜索 CLI…"
aria-label="搜索 CLI"
// 搜索功能属于后续波次,Wave 0 仅为占位
/>
</div>
{warningCount > 0 && (
<button type="button" className="env-chip">
<span className="status-dot warn breathe" aria-hidden="true" />
<span>{warningCount} </span>
<ChevronDown size={12} strokeWidth={1.5} aria-hidden="true" />
</button>
)}
</div>
</header>
);
}
+39
View File
@@ -0,0 +1,39 @@
export type KpiTone = "primary" | "attention" | "accent" | "warning";
const TONE_COLOR: Record<KpiTone, string> = {
primary: "var(--ad-primary)",
attention: "var(--ad-attention)",
accent: "var(--ad-accent)",
warning: "var(--ad-warning)",
};
interface KpiCardProps {
label: string;
value: number;
tone: KpiTone;
/** 副标题:对象点名句式(视觉规范 §3.1.1) */
subtitle: string;
}
/**
* KPI 卡(视觉规范 §3.1.1):
* - 径向渐变玻璃板 + 上缘高光 + --ad-shadow-panel
* - 顶部色条贯通整宽;数值为 0 时色条降灰、数字回 --ad-text-1
* - 数字随状态色贯穿
*/
export function KpiCard({ label, value, tone, subtitle }: KpiCardProps) {
const color = value > 0 ? TONE_COLOR[tone] : "var(--ad-border)";
const valueColor = value > 0 ? TONE_COLOR[tone] : "var(--ad-text-1)";
return (
<div className="kpi-card">
<div className="kpi-bar" style={{ background: color }} aria-hidden="true" />
<div className="kpi-body">
<div className="kpi-value" style={{ color: valueColor }}>
{value}
</div>
<div className="kpi-label">{label}</div>
<div className="kpi-subtitle">{subtitle}</div>
</div>
</div>
);
}
+6
View File
@@ -0,0 +1,6 @@
import type { ReactNode } from "react";
/** 内联等宽 chip(视觉规范 §2.2:文件名/版本号/命令专用) */
export function MonoChip({ children }: { children: ReactNode }) {
return <span className="mono-chip">{children}</span>;
}
+79
View File
@@ -0,0 +1,79 @@
import {
Archive,
Gauge,
Library,
Settings,
SlidersHorizontal,
SquareTerminal,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
export type PageKey =
| "overview"
| "catalog"
| "my-cli"
| "config"
| "backup"
| "settings";
export interface PageMeta {
title: string;
}
export const PAGE_META: Record<PageKey, PageMeta> = {
overview: { title: "总览" },
catalog: { title: "CLI 目录" },
"my-cli": { title: "我的 CLI" },
config: { title: "配置中心" },
backup: { title: "备份与迁移" },
settings: { title: "设置" },
};
interface NavItem {
key: PageKey;
label: string;
icon: LucideIcon;
}
const NAV_ITEMS: NavItem[] = [
{ key: "overview", label: "总览", icon: Gauge },
{ key: "catalog", label: "CLI 目录", icon: Library },
{ key: "my-cli", label: "我的 CLI", icon: SquareTerminal },
{ key: "config", label: "配置中心", icon: SlidersHorizontal },
{ key: "backup", label: "备份与迁移", icon: Archive },
{ key: "settings", label: "设置", icon: Settings },
];
const ENGINE_VERSION = "v0.1.0";
interface SidebarProps {
current: PageKey;
onNavigate: (page: PageKey) => void;
}
/** 左侧竖导航 + 底部引擎状态区(视觉规范 §3.0) */
export function Sidebar({ current, onNavigate }: SidebarProps) {
return (
<aside className="sidebar">
<nav className="sidebar-nav" aria-label="主导航">
{NAV_ITEMS.map(({ key, label, icon: Icon }) => (
<button
key={key}
type="button"
className={current === key ? "nav-item active" : "nav-item"}
onClick={() => onNavigate(key)}
aria-current={current === key ? "page" : undefined}
>
<Icon size={20} strokeWidth={1.5} className="nav-icon" aria-hidden="true" />
<span className="nav-label">{label}</span>
</button>
))}
</nav>
<div className="sidebar-engine">
<span className="status-dot ok" aria-hidden="true" />
<span className="engine-text"> · </span>
<span className="mono-chip engine-version">{ENGINE_VERSION}</span>
</div>
</aside>
);
}
@@ -0,0 +1,18 @@
export type BadgeKind = "installed" | "update" | "auth" | "uninstalled";
const KIND_CLASS: Record<BadgeKind, string> = {
installed: "badge badge-installed",
update: "badge badge-update",
auth: "badge badge-auth",
uninstalled: "badge badge-uninstalled",
};
interface StatusBadgeProps {
kind: BadgeKind;
label: string;
}
/** 状态角标(视觉规范 §3.1.4 / §3.2:已安装=青、可更新=品红+弱辉光、待授权=紫、未安装=灰) */
export function StatusBadge({ kind, label }: StatusBadgeProps) {
return <span className={KIND_CLASS[kind]}>{label}</span>;
}
+38
View File
@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
import { listCatalog } from "../ipc";
import type { CatalogEntry } from "../ipc/types";
export interface UseCatalogResult {
entries: CatalogEntry[];
loading: boolean;
error: string | null;
}
/** 加载适配器目录(listCatalog */
export function useCatalog(): UseCatalogResult {
const [entries, setEntries] = useState<CatalogEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
listCatalog()
.then((list) => {
if (!cancelled) {
setEntries(list);
setLoading(false);
}
})
.catch((err: unknown) => {
if (!cancelled) {
setError(String(err));
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, []);
return { entries, loading, error };
}
+38
View File
@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
import { detectEnv } from "../ipc";
import type { PlatformEnv } from "../ipc/types";
export interface UseEnvResult {
env: PlatformEnv | null;
loading: boolean;
error: string | null;
}
/** 加载本机环境检测结果(一次性的只读快照) */
export function useEnv(): UseEnvResult {
const [env, setEnv] = useState<PlatformEnv | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
detectEnv()
.then((e) => {
if (!cancelled) {
setEnv(e);
setLoading(false);
}
})
.catch((err: unknown) => {
if (!cancelled) {
setError(String(err));
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, []);
return { env, loading, error };
}
+58
View File
@@ -0,0 +1,58 @@
// 类型化 invoke 封装(唯一前端 → Rust 入口,架构 §2「界面层禁直接拼命令」)
import { invoke } from "@tauri-apps/api/core";
import type { CatalogEntry, PlatformEnv } from "./types";
/** 是否运行在 Tauri 环境(否则走 mock,便于纯浏览器联调 UI) */
function isTauri(): boolean {
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
}
export async function detectEnv(): Promise<PlatformEnv> {
if (!isTauri()) {
return mockPlatformEnv();
}
return invoke<PlatformEnv>("detectEnv");
}
export async function listCatalog(): Promise<CatalogEntry[]> {
if (!isTauri()) {
return mockCatalog();
}
return invoke<CatalogEntry[]>("listCatalog");
}
// ---- 浏览器联调用 mock(仅当不在 Tauri 内时生效,不影响真实数据) ----
const mockCatalog = (): CatalogEntry[] => [
{ id: "codex", name: "Codex CLI", name_zh: "Codex CLI", vendor: "OpenAI", status: "available" },
{ id: "claude-code", name: "Claude Code", name_zh: "Claude Code", vendor: "Anthropic", status: "available" },
{ id: "gemini", name: "Gemini CLI", name_zh: "Gemini CLI", vendor: "Google", status: "available" },
{ id: "copilot", name: "Copilot CLI", name_zh: "Copilot CLI", vendor: "GitHub", status: "available" },
{ id: "kimi", name: "Kimi CLI", name_zh: "Kimi CLI", vendor: "月之暗面", status: "available" },
{ id: "qwen", name: "Qwen Code", name_zh: "Qwen Code", vendor: "阿里巴巴", status: "available" },
{ id: "codebuddy", name: "CodeBuddy Code", name_zh: "CodeBuddy Code", vendor: "腾讯", status: "available" },
{ id: "opencode", name: "OpenCode", name_zh: "OpenCode", vendor: "opencode.ai", status: "available" },
{ id: "crush", name: "Crush", name_zh: "Crush", vendor: "Charm", status: "available" },
{ id: "goose", name: "Goose", name_zh: "Goose", vendor: "Linux 基金会(AAIF", status: "available" },
{ id: "aider", name: "Aider", name_zh: "Aider", vendor: "aider.chat", status: "available" },
{ id: "cursor", name: "Cursor CLI", name_zh: "Cursor CLI", vendor: "Cursor", status: "available" },
{ id: "cline", name: "Cline", name_zh: "Cline", vendor: "Cline", status: "available" },
{ id: "warp", name: "Warp Agent CLI", name_zh: "Warp Agent CLI", vendor: "Warp", status: "available" },
];
const mockPlatformEnv = (): PlatformEnv => ({
os: "windows",
os_version: "Windows 11 24H2 (Build 26100)",
arch: "x86_64",
shells: { powershell_version: "5.1.26100.1", pwsh_version: null, bash_available: true },
runtimes: {
node: { status: "installed", version: "24.18.0", path: "C:\\Program Files\\nodejs\\node.exe" },
npm: { status: "installed", version: "11.16.0", path: "C:\\Program Files\\nodejs\\npm.cmd" },
python: { status: "installed", version: "3.14.6", path: "C:\\Python314\\python.exe" },
uv: { status: "not_in_path", version: null, path: null },
git: { status: "installed", version: "2.47.1", path: "C:\\Program Files\\Git\\cmd\\git.exe" },
winget: { status: "installed", version: "1.8.1911", path: "C:\\Users\\MoBai\\AppData\\Local\\Microsoft\\WindowsApps\\winget.exe" },
apt: { status: "not_in_path", version: null, path: null },
},
path_entries: ["C:\\Windows\\system32", "C:\\Windows", "C:\\Program Files\\nodejs"],
capabilities: { keyring: "ok", can_elevate: false },
});
+54
View File
@@ -0,0 +1,54 @@
// IPC 契约类型(对齐架构 §5 PlatformEnv / listCatalog
// 字段命名与 Rust 侧 serde 输出一致(snake_case)。
export interface PlatformEnv {
os: string;
os_version: string;
arch: string;
shells: Shells;
runtimes: Runtimes;
path_entries: string[];
capabilities: Capabilities;
}
export interface Shells {
powershell_version: string | null;
pwsh_version: string | null;
bash_available: boolean;
}
export interface Runtimes {
node: RuntimeInfo | null;
npm: RuntimeInfo | null;
python: RuntimeInfo | null;
uv: RuntimeInfo | null;
git: RuntimeInfo | null;
winget: RuntimeInfo | null;
apt: RuntimeInfo | null;
}
export type RuntimeStatus =
| "installed"
| "not_installed"
| "not_in_path"
| "exec_failed"
| "version_unparseable";
export interface RuntimeInfo {
status: RuntimeStatus;
version: string | null;
path: string | null;
}
export interface Capabilities {
keyring: string;
can_elevate: boolean;
}
export interface CatalogEntry {
id: string;
name: string;
name_zh: string;
vendor: string;
status: string;
}
+30
View File
@@ -0,0 +1,30 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
// 设计 Token(视觉规范 v1.2,唯一品味依据)
import "./tokens/color.css";
import "./tokens/typography.css";
import "./tokens/space.css";
import "./tokens/motion.css";
import "./tokens/effects.css";
import "./tokens/tiers.css";
import "./styles/global.css";
/** 性能档位自动判定(架构 §8.3 / 视觉规范 §5) */
function applyFxTier(): void {
const html = document.documentElement;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const cores = navigator.hardwareConcurrency ?? 8;
if (reduce || cores <= 4) {
html.dataset.fxTier = "low";
}
}
applyFxTier();
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
+19
View File
@@ -0,0 +1,19 @@
import { Archive } from "lucide-react";
/** 备份与迁移(PRD §7):创建备份、查看内容、恢复。Wave 0 占位。 */
export function BackupPage() {
return (
<div className="placeholder-page">
<div className="placeholder-icon">
<Archive size={40} strokeWidth={1.5} aria-hidden="true" />
</div>
<h2 className="placeholder-title"></h2>
<p className="placeholder-desc">
Wave 4
</p>
<button type="button" className="btn btn-secondary" disabled>
</button>
</div>
);
}
+113
View File
@@ -0,0 +1,113 @@
import { useMemo, useState } from "react";
import { Search } from "lucide-react";
import { useCatalog } from "../hooks/useCatalog";
import { StatusBadge } from "../components/StatusBadge";
import { MonoChip } from "../components/MonoChip";
import { CliMonogram } from "../components/CliMonogram";
type StatusFilter = "all" | "installed" | "uninstalled" | "update";
type PlatformFilter = "all" | "windows" | "linux";
const STATUS_FILTERS: { key: StatusFilter; label: string }[] = [
{ key: "all", label: "全部" },
{ key: "installed", label: "已安装" },
{ key: "uninstalled", label: "未安装" },
{ key: "update", label: "可更新" },
];
const PLATFORM_FILTERS: { key: PlatformFilter; label: string }[] = [
{ key: "all", label: "全部平台" },
{ key: "windows", label: "Windows" },
{ key: "linux", label: "Linux" },
];
/** CLI 目录页(视觉规范 §3.2 + PRD §7):搜索 + 筛选 + 14 张卡片(数据来自 catalog.yaml */
export function CatalogPage() {
const { entries, loading, error } = useCatalog();
const [query, setQuery] = useState("");
const [status, setStatus] = useState<StatusFilter>("all");
const [platform, setPlatform] = useState<PlatformFilter>("all");
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
return entries.filter((e) => {
if (q) {
const haystack = `${e.name_zh} ${e.name} ${e.vendor} ${e.id}`.toLowerCase();
if (!haystack.includes(q)) return false;
}
// Wave 0:14 个均为未安装;已安装/可更新筛选结果为空(诚实占位)
if (status === "installed") return false;
if (status === "update") return false;
// 平台:全部 14 个均支持 Windows + Ubuntu(调研底稿结论),筛选不改变集合
if (platform === "windows" || platform === "linux") return true;
return true;
});
}, [entries, query, status, platform]);
return (
<div className="catalog">
<div className="catalog-toolbar">
<div className="search-box">
<Search size={16} strokeWidth={1.5} className="search-icon" aria-hidden="true" />
<input
type="search"
placeholder="搜索 CLI…"
value={query}
onChange={(e) => setQuery(e.target.value)}
aria-label="搜索 CLI"
/>
</div>
<div className="filter-group" role="group" aria-label="状态筛选">
{STATUS_FILTERS.map((f) => (
<button
key={f.key}
type="button"
className={status === f.key ? "pill active" : "pill"}
onClick={() => setStatus(f.key)}
>
{f.label}
</button>
))}
</div>
<div className="filter-group" role="group" aria-label="平台筛选">
{PLATFORM_FILTERS.map((f) => (
<button
key={f.key}
type="button"
className={platform === f.key ? "pill active" : "pill"}
onClick={() => setPlatform(f.key)}
>
{f.label}
</button>
))}
</div>
</div>
{error && <p className="panel-empty">{error}</p>}
{loading && !error && <p className="panel-empty"></p>}
<div className="catalog-grid">
{filtered.map((entry) => (
<article className="catalog-card" key={entry.id}>
<div className="catalog-card-top">
<CliMonogram name={entry.name} size={40} />
<div className="catalog-card-head">
<div className="catalog-card-name">{entry.name_zh}</div>
<div className="catalog-card-vendor">{entry.vendor}</div>
</div>
</div>
<p className="catalog-card-desc"> · Wave 1 </p>
<div className="catalog-card-bottom">
<StatusBadge kind="uninstalled" label="未安装" />
<span className="catalog-platforms">Windows · Linux</span>
<MonoChip>v1</MonoChip>
</div>
</article>
))}
{!loading && filtered.length === 0 && (
<p className="panel-empty catalog-empty"> CLI</p>
)}
</div>
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { SlidersHorizontal } from "lucide-react";
/** 配置中心(PRD §7):按 CLI 展示中文配置表单。Wave 0 占位。 */
export function ConfigCenterPage() {
return (
<div className="placeholder-page">
<div className="placeholder-icon">
<SlidersHorizontal size={40} strokeWidth={1.5} aria-hidden="true" />
</div>
<h2 className="placeholder-title"></h2>
<p className="placeholder-desc">
Wave 2 使
</p>
<button type="button" className="btn btn-secondary" disabled>
CLI
</button>
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { SquareTerminal } from "lucide-react";
/** 我的 CLIPRD §7):版本 / 路径 / 授权状态。Wave 0 尚无已安装 CLI,展示空态。 */
export function MyCliPage() {
return (
<div className="placeholder-page">
<div className="placeholder-icon">
<SquareTerminal size={40} strokeWidth={1.5} aria-hidden="true" />
</div>
<h2 className="placeholder-title"> CLI</h2>
<p className="placeholder-desc">
Wave 1 CLI
</p>
<button type="button" className="btn btn-primary" disabled>
CLI
</button>
</div>
);
}
+254
View File
@@ -0,0 +1,254 @@
import { useState } from "react";
import { Activity, Archive, ChevronRight, Lock, Plus } from "lucide-react";
import { useEnv } from "../hooks/useEnv";
import { useCatalog } from "../hooks/useCatalog";
import { KpiCard } from "../components/KpiCard";
import { MonoChip } from "../components/MonoChip";
import { StatusBadge } from "../components/StatusBadge";
import { CliMonogram } from "../components/CliMonogram";
import type { PlatformEnv, RuntimeInfo } from "../ipc/types";
/** 平台相关运行时集合(Windows 不看 aptLinux 不看 winget */
const WINDOWS_RUNTIMES = ["node", "npm", "python", "uv", "git", "winget"] as const;
const LINUX_RUNTIMES = ["node", "npm", "python", "uv", "git", "apt"] as const;
function runtimeProblems(env: PlatformEnv): { name: string; info: RuntimeInfo }[] {
const keys = env.os === "windows" ? WINDOWS_RUNTIMES : LINUX_RUNTIMES;
const problems: { name: string; info: RuntimeInfo }[] = [];
for (const key of keys) {
const info = env.runtimes[key];
if (info && info.status !== "installed") {
problems.push({ name: key, info });
}
}
return problems;
}
function runtimeStatusText(status: string): string {
switch (status) {
case "not_in_path":
return "不在 PATH";
case "not_installed":
return "未安装";
case "exec_failed":
return "执行失败";
case "version_unparseable":
return "版本未知";
default:
return "未知";
}
}
function RuntimeItem({ name, info }: { name: string; info: RuntimeInfo }) {
const dotClass =
info.status === "installed"
? "status-dot ok"
: info.status === "exec_failed"
? "status-dot error"
: "status-dot warn";
return (
<div className="runtime-item">
<span className={dotClass} aria-hidden="true" />
<span className="runtime-name">{name}</span>
{info.version ? (
<MonoChip>{info.version}</MonoChip>
) : (
<span className="runtime-missing">{runtimeStatusText(info.status)}</span>
)}
</div>
);
}
function EnvPanel() {
const { env, loading, error } = useEnv();
const [showAllPath, setShowAllPath] = useState(false);
if (error) {
return (
<section className="panel env-panel">
<div className="panel-head">
<h2 className="panel-title"></h2>
</div>
<p className="panel-empty">{error}</p>
</section>
);
}
const pathPreview = env ? env.path_entries.slice(0, 8) : [];
return (
<section className="panel env-panel">
<div className="panel-head">
<h2 className="panel-title"></h2>
{loading && <span className="panel-hint"></span>}
</div>
{env && (
<>
<div className="env-grid">
<div className="env-basic">
<div className="env-row">
<span className="env-key"></span>
<span className="env-val">{env.os_version}</span>
</div>
<div className="env-row">
<span className="env-key"></span>
<span className="env-val">{env.arch}</span>
</div>
<div className="env-row">
<span className="env-key">Shell</span>
<span className="env-val">
{env.shells.powershell_version ? (
<>PowerShell <MonoChip>{env.shells.powershell_version}</MonoChip></>
) : null}
{env.shells.pwsh_version ? (
<> · pwsh <MonoChip>{env.shells.pwsh_version}</MonoChip></>
) : null}
{env.shells.bash_available ? " · Bash ✓" : ""}
</span>
</div>
<div className="env-row">
<span className="env-key"></span>
<span className="env-val">
{env.capabilities.keyring === "ok" ? "Credential Manager ✓" : "密钥库缺失"}
</span>
</div>
<div className="env-row">
<span className="env-key"></span>
<span className="env-val">{env.capabilities.can_elevate ? "可用" : "不可用"}</span>
</div>
</div>
<div className="runtime-grid">
{(["node", "npm", "python", "uv", "git", "winget", "apt"] as const).map(
(key) => {
const info = env.runtimes[key];
if (!info) return null;
return <RuntimeItem key={key} name={key} info={info} />;
},
)}
</div>
</div>
<div className="path-block">
<div className="path-head">
<span className="path-title">
PATH{env.path_entries.length}
<MonoChip>{env.path_entries.length}</MonoChip>
</span>
{env.path_entries.length > pathPreview.length && (
<button
type="button"
className="link-btn"
onClick={() => setShowAllPath((v) => !v)}
>
{showAllPath ? "收起" : "展开全部"}
</button>
)}
</div>
{(showAllPath ? env.path_entries : pathPreview).map((entry, idx) => (
<div key={idx} className="path-entry">
{entry}
</div>
))}
</div>
</>
)}
</section>
);
}
/** 总览页(视觉规范 §3.1 + PRD §7):KPI + 最近诊断/快速操作 + CLI 状态网格 + 本机环境 */
export function OverviewPage() {
const { env } = useEnv();
const { entries } = useCatalog();
const warningCount = env ? runtimeProblems(env).length : 0;
const warningNames = env ? runtimeProblems(env).map((p) => p.name) : [];
return (
<div className="overview">
{/* ① KPI 一排 4 张 */}
<section className="kpi-grid" aria-label="概览指标">
<KpiCard label="已安装" value={0} tone="primary" subtitle="暂无已安装 CLI" />
<KpiCard label="待授权" value={0} tone="attention" subtitle="暂无待授权项" />
<KpiCard label="可更新" value={0} tone="accent" subtitle="暂无可用更新" />
<KpiCard
label="环境异常"
value={warningCount}
tone="warning"
subtitle={warningCount > 0 ? warningNames.join(" · ") : "环境正常"}
/>
</section>
{/* ② 中段:左 2/3 最近诊断 + 右 1/3 快速操作 */}
<section className="overview-mid">
<div className="panel diag-panel">
<div className="panel-head">
<h2 className="panel-title"></h2>
<button type="button" className="link-btn">
<ChevronRight size={14} strokeWidth={1.5} aria-hidden="true" />
</button>
</div>
<ul className="diag-list">
<li className="diag-row">
<span className="status-dot ok" aria-hidden="true" />
<span className="diag-cli"></span>
<span className="diag-text"> Wave 1 </span>
<span className="diag-time"></span>
</li>
</ul>
</div>
<div className="panel quick-card">
<h2 className="panel-title"></h2>
<button type="button" className="btn btn-primary" disabled>
<Plus size={16} strokeWidth={1.5} aria-hidden="true" /> CLI
</button>
<button type="button" className="btn btn-secondary" disabled>
<Activity size={16} strokeWidth={1.5} aria-hidden="true" />
</button>
<button type="button" className="btn btn-secondary" disabled>
<Archive size={16} strokeWidth={1.5} aria-hidden="true" />
</button>
<div className="recent-installs">
<div className="group-label"></div>
<p className="group-empty"></p>
</div>
<p className="safety-note">
<Lock size={12} strokeWidth={1.5} aria-hidden="true" />
</p>
</div>
</section>
{/* ③ 下段:CLI 状态网格通栏 */}
<section className="panel cli-status-panel">
<div className="panel-head">
<h2 className="panel-title">CLI </h2>
<button type="button" className="link-btn">
{entries.length}
<ChevronRight size={14} strokeWidth={1.5} aria-hidden="true" />
</button>
</div>
<div className="cli-status-grid">
{entries.map((entry) => (
<div className="cli-block" key={entry.id}>
<div className="cli-block-top">
<CliMonogram name={entry.name} size={32} />
<span className="cli-block-name">{entry.name_zh}</span>
<StatusBadge kind="uninstalled" label="未安装" />
</div>
<div className="cli-block-version">
<MonoChip></MonoChip>
</div>
<div className="cli-block-auth">
<span className="status-dot unknown" aria-hidden="true" />
<span> · </span>
</div>
</div>
))}
</div>
</section>
{/* ④ 本机环境(真实检测结果,架构 §5) */}
<EnvPanel />
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
import { Settings } from "lucide-react";
/** 设置(PRD §7):下载源策略 / 自动检查更新 / 语言 / 日志与隐私。Wave 0 静态占位。 */
export function SettingsPage() {
return (
<div className="settings-page">
<h2 className="panel-title"></h2>
<div className="settings-group">
<div className="setting-row">
<div className="setting-info">
<div className="setting-label"></div>
<div className="setting-hint"> CLI Wave 1 </div>
</div>
<span className="setting-value"></span>
</div>
<div className="setting-row">
<div className="setting-info">
<div className="setting-label"></div>
<div className="setting-hint"> AgentDock CLI Wave 4 </div>
</div>
<span className="setting-value"></span>
</div>
<div className="setting-row">
<div className="setting-info">
<div className="setting-label"></div>
<div className="setting-hint"></div>
</div>
<span className="setting-value"></span>
</div>
<div className="setting-row">
<div className="setting-info">
<div className="setting-label"></div>
<div className="setting-hint">Wave 1 </div>
</div>
<span className="setting-value"></span>
</div>
</div>
<div className="settings-page-foot">
<Settings size={14} strokeWidth={1.5} aria-hidden="true" />
AgentDock v0.1.0 · ·
</div>
</div>
);
}
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
/* ============================================================
* 设计 Token · 配色(color
* 来源:视觉规范 v1.2 §2.1(施工唯一品味依据)
* 禁止在此文件之外出现任何魔法色值;改动需先征得总管同意。
* ============================================================ */
:root {
/* ---- 底色(深空阶梯,由深到浅)---- */
--ad-bg-0: #070B14; /* 应用窗口底色 */
--ad-bg-1: #0D1420; /* 面板、卡片底色 */
--ad-bg-2: #131C2E; /* 浮层、弹窗、下拉、mono chip 底 */
--ad-bg-3: #1A2438; /* 悬浮态、按下态的填充 */
--ad-border: #22304A; /* 常规 1px 分隔线 */
--ad-border-bright: #35507A; /* 悬浮/选中的边框 */
/* ---- 霓虹强调色(全系统只有这两个霓虹色)---- */
--ad-primary: #00E5FF; /* 主操作、链接、聚焦、选中态(青) */
--ad-primary-hover: #4DEFFF; /* 主操作悬浮 */
--ad-primary-pressed: #00B4CC; /* 主操作按下 */
--ad-primary-dim: rgba(0, 229, 255, 0.12); /* 选中项底、标签底 */
--ad-accent: #FF4DDB; /* 可更新标记、重要提醒(品红,克制使用) */
--ad-accent-dim: rgba(255, 77, 219, 0.12); /* 品红标签底 */
/* ---- 状态色(诊断四级 + 通用语义,六色语义表 v1.2 定稿)---- */
--ad-success: #2EE6A8; /* 正常 / 已安装 / 已授权 / 已自动修复 */
--ad-attention: #A78BFA; /* 需要留意:提示级 + 待授权 / 未授权(紫) */
--ad-warning: #FFB020; /* 警告 / 需要你注意的异常 */
--ad-danger: #FF4D5E; /* 错误 / 需要你处理 / 高危操作 */
--ad-success-dim: rgba(46, 230, 168, 0.12); /* 状态色 12% 透明底色 */
--ad-attention-dim: rgba(167, 139, 250, 0.12); /* 状态色 12% 透明底色 */
--ad-warning-dim: rgba(255, 176, 32, 0.12); /* 状态色 12% 透明底色 */
--ad-danger-dim: rgba(255, 77, 94, 0.12); /* 状态色 12% 透明底色 */
/* ---- 文字 ---- */
--ad-text-1: #E8EEF7; /* 主文案 */
--ad-text-2: #9AA8BD; /* 次要说明(承载信息的最低允许色,对比度 ≥ 4.5:1) */
--ad-text-3: #5E6E87; /* 仅占位、禁用、纯装饰(禁止承载信息) */
/* ---- 侧边栏选中态(v1.2 §3.0:左缘生长光晕,无独立指示条)---- */
--ad-nav-selected-bg: linear-gradient(
90deg,
rgba(0, 229, 255, 0.30),
rgba(0, 229, 255, 0.12) 45%,
rgba(0, 229, 255, 0.05)
);
--ad-nav-selected-border: inset 0 0 0 1px rgba(0, 229, 255, 0.38);
/* ---- 组件表面(§3.0 / §3.1 明示值)---- */
--ad-surface-card: radial-gradient(120% 100% at 50% 0%, #16233A 0%, #0D1420 60%); /* KPI 卡悬浮玻璃板 */
--ad-card-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06); /* 卡片上缘高光 */
--ad-warning-chip-border: rgba(255, 176, 32, 0.5); /* 环境警告 chip 描边 */
/* ---- 状态角标描边(§3.1.4 / §3.2 明示值)---- */
--ad-primary-border: rgba(0, 229, 255, 0.4);
--ad-accent-border: rgba(255, 77, 219, 0.5);
--ad-attention-border: rgba(167, 139, 250, 0.5);
--ad-success-border: rgba(46, 230, 168, 0.4);
}
+39
View File
@@ -0,0 +1,39 @@
/* ============================================================
* 设计 Token · 阴影与辉光(effects
* 来源:视觉规范 v1.2 §2.4 / §2.6 / §3.0
* 辉光只给:主按钮、选中导航项、聚焦输入框、需处理状态指示灯、
* 可更新角标、环境警告 chip。列表每行都发光 = 全部不发光,禁止。
* ============================================================ */
:root {
/* ---- 阴影 ---- */
--ad-shadow-panel: 0 8px 24px rgba(0, 0, 0, 0.45);
--ad-shadow-pop: 0 12px 40px rgba(0, 0, 0, 0.6);
/* ---- 辉光 ---- */
--ad-glow-primary: 0 0 12px rgba(0, 229, 255, 0.35), 0 0 32px rgba(0, 229, 255, 0.12);
--ad-glow-accent: 0 0 12px rgba(255, 77, 219, 0.35);
--ad-glow-warning: 0 0 10px rgba(255, 176, 32, 0.28); /* 仅警告 chip / 警告状态灯 */
--ad-glow-danger: 0 0 12px rgba(255, 77, 94, 0.35);
/* ---- 主按钮(§2.6v1.2 hover 双层扩散辉光)---- */
--ad-btn-primary-bg: linear-gradient(180deg, #33D9EE 0%, #06BCD9 55%, #049EBB 100%);
--ad-btn-primary-text: #03252E;
--ad-btn-primary-inner: inset 0 1px 0 rgba(255, 255, 255, 0.30),
inset 0 -1px 0 rgba(2, 20, 26, 0.35);
--ad-btn-primary-glow: 0 0 10px rgba(0, 229, 255, 0.22);
--ad-btn-primary-glow-hover: 0 0 14px rgba(0, 229, 255, 0.30),
0 0 36px rgba(0, 229, 255, 0.12);
/* 品红更新按钮(全应用唯一允许品红做按钮的场景) */
--ad-btn-accent-bg: linear-gradient(180deg, #F26AD9 0%, #D633B8 55%, #B5209C 100%);
--ad-btn-accent-glow-hover: 0 0 14px rgba(255, 77, 219, 0.30),
0 0 36px rgba(255, 77, 219, 0.12);
/* ---- 背景网格(§3.0:要么可见要么不做)---- */
--ad-grid-color: rgba(0, 229, 255, 0.05); /* 透明度固定 5%,施工员不许再调 */
--ad-grid-size: 40px;
--ad-grid-mask: radial-gradient(120% 90% at 50% 40%, #000 30%, transparent 75%);
/* ---- 弹窗遮罩(§3.6---- */
--ad-modal-mask: rgba(4, 7, 13, 0.7);
}
+21
View File
@@ -0,0 +1,21 @@
/* ============================================================
* 设计 Token · 动效(motion
* 来源:视觉规范 v1.2 §2.5 / §4
* 通则:只动 transform 与 opacity;辉光可用 box-shadow 过渡但仅限单个聚焦元素。
* ============================================================ */
:root {
/* ---- 缓动曲线 ---- */
--ad-ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* 进场、浮现,主力曲线 */
--ad-ease-inout: cubic-bezier(0.65, 0, 0.35, 1); /* 展开收起、位移、呼吸 */
--ad-ease-linear: linear; /* 仅环境循环动画 */
/* ---- 时长 ---- */
--ad-dur-micro: 120ms; /* 按下、勾选 */
--ad-dur-fast: 200ms; /* 悬浮、淡入淡出 */
--ad-dur-med: 320ms; /* 面板展开、页面切换 */
--ad-dur-slow: 600ms; /* 仅入场编排,一次性 */
/* ---- 状态点呼吸(§4.5---- */
--ad-breathe-duration: 2400ms;
--ad-breathe-min-opacity: 0.4; /* 透明度下限 40%,再低像灭了 */
}
+24
View File
@@ -0,0 +1,24 @@
/* ============================================================
* 设计 Token · 间距 / 圆角(space
* 来源:视觉规范 v1.2 §2.3
* 赛博感靠小圆角 + 发光描边,不做大圆角。
* ============================================================ */
:root {
/* ---- 间距(4px 基数)---- */
--ad-space-1: 4px;
--ad-space-2: 8px;
--ad-space-3: 12px;
--ad-space-4: 16px;
--ad-space-5: 20px;
--ad-space-6: 24px;
--ad-space-8: 32px;
--ad-space-10: 40px;
/* ---- 圆角 ---- */
--ad-radius-s: 4px; /* 标签、小按钮、chip */
--ad-radius-m: 8px; /* 卡片、输入框、弹窗 */
--ad-radius-l: 12px; /* 主面板 */
/* ---- 描边 ---- */
--ad-border-width: 1px; /* 描边统一 1px;发光描边 = 1px 实色 + 外辉光,不加粗 */
}
+59
View File
@@ -0,0 +1,59 @@
/* ============================================================
* 设计 Token · 性能档位(tiers
* 来源:视觉规范 v1.2 §5(性能降级策略)/ 架构 §8.3
*
* 三档:高 / 中 / 低,默认 balanced(自动降档由 JS 判定后设置
* data-fx-tier 属性,见 src/main.tsx)。
* - prefers-reduced-motion 强制 low
* - low:仅 L0 + 必要过渡,无循环动画、无辉光、无切角
* ============================================================ */
:root {
--ad-fx-tier: balanced;
}
@media (prefers-reduced-motion: reduce) {
:root {
--ad-fx-tier: low;
}
}
/* ---- 低档:移除辉光、循环动画、过渡、切角 ---- */
[data-fx-tier="low"] *,
[data-fx-tier="low"] *::before,
[data-fx-tier="low"] *::after {
animation: none !important;
transition: none !important;
box-shadow: none !important;
}
[data-fx-tier="low"] .clip-notch {
clip-path: none !important;
}
/* ---- 中档:只保留单层辉光(半径减半),循环动画停用 ---- */
[data-fx-tier="balanced"] .fx-multilayer-glow {
box-shadow: 0 0 6px rgba(0, 229, 255, 0.3);
}
/* 背景网格漂移(§4.8,60s 一周,仅高档/中档) */
[data-fx-tier="low"] .grid-drift {
animation: none;
}
.grid-drift {
animation: grid-drift 60s linear infinite;
}
@keyframes grid-drift {
from { background-position: 0 0; }
to { background-position: 40px 40px; }
}
/* 状态点呼吸(§4.5:仅警告/错误/授权可能过期呼吸;低档停用) */
[data-fx-tier="low"] .status-dot-breathe {
animation: none;
}
.status-dot-breathe {
animation: ad-breathe var(--ad-dur-slow) var(--ad-ease-inout) infinite;
}
@keyframes ad-breathe {
0%, 100% { opacity: 1; }
50% { opacity: var(--ad-breathe-min-opacity); }
}
+31
View File
@@ -0,0 +1,31 @@
/* ============================================================
* 设计 Token · 字体(typography
* 来源:视觉规范 v1.2 §2.2
* 不加任何外联 Web 字体(离线可用、安装包不膨胀)。
* ============================================================ */
:root {
/* ---- 字体栈 ---- */
--ad-font-ui: "PingFang SC", "Microsoft YaHei UI", "Microsoft YaHei",
"Noto Sans CJK SC", "Source Han Sans SC", system-ui, sans-serif;
--ad-font-mono: "Cascadia Code", "JetBrains Mono", Consolas,
"Noto Sans Mono CJK SC", monospace;
/* ---- 字阶(字号 / 行高 / 字重)---- */
--ad-text-xs-size: 12px; --ad-text-xs-height: 18px; --ad-text-xs-weight: 400;
--ad-text-s-size: 13px; --ad-text-s-height: 20px; --ad-text-s-weight: 400;
--ad-text-m-size: 14px; --ad-text-m-height: 22px; --ad-text-m-weight: 400;
--ad-text-l-size: 16px; --ad-text-l-height: 24px; --ad-text-l-weight: 500;
--ad-text-xl-size: 20px; --ad-text-xl-height: 28px; --ad-text-xl-weight: 600;
--ad-text-num-size: 28px; --ad-text-num-height: 36px; --ad-text-num-weight: 600;
/* ---- Mono chip(§2.2 对比度三件套之二)----
* 正文/列表中内联出现的文件名、版本号、命令一律套此样式:
* 12px 等宽、--ad-bg-2 底、1px --ad-border、--ad-radius-s、内边距 1px 6px、文字 --ad-text-1 */
--ad-mono-chip-font: var(--ad-font-mono);
--ad-mono-chip-size: 12px;
--ad-mono-chip-bg: var(--ad-bg-2);
--ad-mono-chip-border: 1px solid var(--ad-border);
--ad-mono-chip-radius: var(--ad-radius-s);
--ad-mono-chip-padding: 1px 6px;
--ad-mono-chip-color: var(--ad-text-1);
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2021",
"useDefineForClassFields": true,
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+28
View File
@@ -0,0 +1,28 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
const host = process.env.TAURI_DEV_HOST;
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
// Tauri 开发约定:固定端口 1420,避免占用冲突
clearScreen: false,
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 不监听 src-tauri,避免 Rust 改动触发前端重载
ignored: ["**/src-tauri/**"],
},
},
});