feat: improve app ui and project metadata

This commit is contained in:
Yaojia Wang
2026-06-28 07:31:20 +02:00
parent 3bd464d400
commit 90a66437d7
86 changed files with 4892 additions and 1108 deletions

View File

@@ -1,6 +1,20 @@
import type { LucideIcon } from "lucide-react";
import { Blocks, Database, Pencil, ShieldCheck } from "lucide-react";
import { AppShell } from "@/components/AppShell";
import { Badge } from "@/components/ui/Badge";
import { Card } from "@/components/ui/Card";
import { EmptyState } from "@/components/ui/EmptyState";
import { PageHeader } from "@/components/ui/PageHeader";
import { SectionHeader } from "@/components/ui/SectionHeader";
import { StatusNote } from "@/components/ui/StatusNote";
import type { ProjectResponse, SkillView } from "@/lib/api/types";
import { groupByScope, scopeLabel, tierLabel } from "@/lib/skills/skills";
import {
groupByScope,
scopeLabel,
summarizeSkills,
tierLabel,
} from "@/lib/skills/skills";
interface SkillsPageProps {
project: ProjectResponse;
@@ -10,6 +24,7 @@ interface SkillsPageProps {
// 技能库UX §7只读注册表视图name/scope/tier/reads/writes。Server Component纯读
export function SkillsPage({ project, skills }: SkillsPageProps) {
const groups = groupByScope(skills);
const summary = summarizeSkills(skills);
return (
<AppShell
@@ -18,60 +33,161 @@ export function SkillsPage({ project, skills }: SkillsPageProps) {
projectId={project.id}
activeNav="skills"
>
<div className="mx-auto flex max-w-4xl flex-col gap-6 p-6">
<h1 className="font-serif text-lg text-ink"></h1>
<p className="text-xs text-ink-soft">
Skill /
</p>
{groups.length === 0 ? (
<p className="text-sm text-ink-soft"></p>
) : (
groups.map((group) => (
<section key={group.scope}>
<h2 className="mb-2 font-serif text-sm text-ink">
{scopeLabel(group.scope)}
<span className="ml-2 text-xs text-ink-soft">
{group.skills.length}
</span>
</h2>
<ul className="flex flex-col gap-2">
{group.skills.map((skill) => (
<li
key={skill.name}
className="rounded border border-line bg-panel p-3 text-sm"
>
<div className="mb-1 flex flex-wrap items-center gap-2">
<span className="font-mono text-ink">{skill.name}</span>
<span className="rounded bg-bg px-2 py-0.5 text-xs text-ink-soft">
{tierLabel(skill.tier)}
<div className="mx-auto flex max-w-6xl flex-col gap-6 p-6">
<PageHeader
title="技能库"
description="声明式 Skill 注册表:每个技能声明能力档位与可读/写的表,越权产出会被丢弃并审计。"
/>
<div className="grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
<aside className="flex flex-col gap-4 lg:sticky lg:top-[8rem] lg:self-start">
<Card as="section" className="p-4">
<SectionHeader
title="注册表概览"
description="按来源、档位和表权限汇总当前可用技能。"
/>
<dl className="mt-4 grid grid-cols-3 gap-2 text-center">
<div className="rounded border border-line bg-paper p-3">
<dt className="text-xs text-ink-soft"></dt>
<dd className="mt-1 font-serif text-2xl text-ink">
{summary.total}
</dd>
</div>
<div className="rounded border border-line bg-paper p-3">
<dt className="text-xs text-ink-soft"></dt>
<dd className="mt-1 font-serif text-2xl text-ink">
{summary.writableCount}
</dd>
</div>
<div className="rounded border border-line bg-paper p-3">
<dt className="text-xs text-ink-soft"></dt>
<dd className="mt-1 font-serif text-2xl text-ink">
{summary.readonlyCount}
</dd>
</div>
</dl>
<div className="mt-4 flex flex-wrap gap-2">
{summary.scopes.length > 0 ? (
summary.scopes.map((item) => (
<Badge key={item.key} variant="info">
{scopeLabel(item.key)} {item.count}
</Badge>
))
) : (
<span className="text-xs text-ink-soft"></span>
)}
</div>
</Card>
<Card as="section" className="p-4">
<SectionHeader title="能力档位" />
{summary.tiers.length > 0 ? (
<ul className="mt-3 flex flex-col gap-2 text-sm">
{summary.tiers.map((item) => (
<li
key={item.key}
className="flex items-center justify-between border-b border-line/70 py-2 last:border-b-0"
>
<span className="text-ink">{tierLabel(item.key)}</span>
<span className="font-mono text-xs text-ink-soft">
{item.count}
</span>
{skill.genre ? (
<span className="rounded bg-bg px-2 py-0.5 text-xs text-ink-soft">
{skill.genre}
</span>
) : null}
</div>
<div className="flex flex-wrap gap-4 text-xs text-ink-soft">
<span>
{skill.reads && skill.reads.length > 0
? skill.reads.join("、")
: "—"}
</span>
<span>
{skill.writes && skill.writes.length > 0
? skill.writes.join("、")
: "—"}
</span>
</div>
</li>
</li>
))}
</ul>
) : (
<p className="mt-3 text-sm text-ink-soft"></p>
)}
</Card>
<StatusNote title="写入边界" variant="info">
<p className="text-sm leading-6">
{formatTables(summary.writableTables)}
</p>
<p className="mt-1 text-xs leading-5 text-ink-soft">
稿
</p>
</StatusNote>
</aside>
<div className="flex min-w-0 flex-col gap-5">
{groups.length === 0 ? (
<EmptyState
icon={Blocks}
title="暂无已注册技能"
description="技能注册后会在这里按作用域展示能力档位、可读表与可写表。"
/>
) : (
<>
{groups.map((group) => (
<section key={group.scope}>
<SectionHeader
title={scopeLabel(group.scope)}
description={`${group.skills.length} 个技能`}
/>
<ul className="mt-3 grid gap-3">
{group.skills.map((skill) => (
<SkillListItem key={skill.name} skill={skill} />
))}
</ul>
</section>
))}
</ul>
</section>
))
)}
</>
)}
</div>
</div>
</div>
</AppShell>
);
}
function SkillListItem({ skill }: { skill: SkillView }) {
return (
<li className="rounded border border-line bg-panel p-4 text-sm shadow-paper">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="break-all font-mono text-ink">{skill.name}</span>
<Badge variant="info">{tierLabel(skill.tier)}</Badge>
{skill.genre ? <Badge>{skill.genre}</Badge> : null}
</div>
<div className="mt-3 grid gap-2 text-xs text-ink-soft md:grid-cols-2">
<TableLine
icon={Database}
label="读"
tables={skill.reads ?? []}
/>
<TableLine icon={Pencil} label="写" tables={skill.writes ?? []} />
</div>
</div>
<div className="flex shrink-0 items-center gap-2 text-xs text-ink-soft">
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
<span>
{skill.writes && skill.writes.length > 0 ? "受控写入" : "只读"}
</span>
</div>
</div>
</li>
);
}
function TableLine({
icon: Icon,
label,
tables,
}: {
icon: LucideIcon;
label: string;
tables: readonly string[];
}) {
return (
<span className="flex min-w-0 items-start gap-1.5">
<Icon className="mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span className="shrink-0">{label}</span>
<span className="min-w-0 break-words">{formatTables(tables)}</span>
</span>
);
}
function formatTables(tables: readonly string[]): string {
return tables.length > 0 ? tables.join("、") : "—";
}