/** * 工作台后四要素的共用小件 —— 自 `~/coding/JCAI_PRD/workbench/_components/ModuleWorkspace.tsx` * 第 21–160 行平移。**DOM 与 `jc-*` 类名原样保留**(README 平移规则 1/2/4)。 * * 四要素同款三栏布局:左清单 `task-sidebar` / 中文档卡 `execution-panel` / 右会话 `session-chat`。 */ import type { ReactNode } from 'react'; import { Ico } from '@web-shared/Ico.tsx'; import ScrollArea from '@web-shared/ScrollArea.tsx'; /** 首字头像:名字 hash 取渐变色;外文名取首字母大写 */ const AVA_GRAD: ReadonlyArray = [ ['#2bc7d4', '#0e9fd8'], ['#3370ff', '#245bdb'], ['#7f3bf5', '#5b21cf'], ['#ff8800', '#de7802'], ['#0aa294', '#0a6a64'], ['#f14ba9', '#c92d86'], ]; function avaOf(name: string): { ch: string; grad: readonly [string, string] } { let h = 0; for (const c of name) h = (h * 31 + c.charCodeAt(0)) >>> 0; const first = name[0] ?? '·'; const ch = /[A-Za-z]/.test(first) ? first.toUpperCase() : first; return { ch, grad: AVA_GRAD[h % AVA_GRAD.length]! }; } export function Avatar({ name, size = 30 }: { name: string; size?: number }) { const { ch, grad } = avaOf(name); return ( {ch} ); } export function AvatarStack({ names, size = 26 }: { names: readonly string[]; size?: number }) { const show = names.slice(0, 5); return ( {show.map((n, i) => ( ))} {names.length > show.length && +{names.length - show.length}} ); } export type Tone = 'todo' | 'doing' | 'done' | 'warn' | 'brand' | 'ok'; export function Chip({ tone, label, pulse }: { tone: Tone; label: string; pulse?: boolean }) { return ( {pulse && } {label} ); } /** 文档头(渐变图标块 + 标题 + 元信息 + 右上状态章)—— 统一容器,彻底告别贴边 */ export function Hero({ grad, icon, kicker, title, meta, chip, }: { grad: readonly [string, string]; icon: string; kicker?: string; title: string; meta: ReactNode; chip?: ReactNode; }) { return (
{kicker && {kicker}}

{title}

{meta}
{chip &&
{chip}
}
); } export function Section({ icon, title, extra, children, }: { icon: string; title: string; extra?: ReactNode; children: ReactNode; }) { return (
{title} {extra && {extra}}
{children}
); } /** 侧栏表头 —— 四要素共用 */ export function SidebarHead({ icon, title, sub }: { icon: string; title: string; sub: string }) { return (

{title}

{sub}
); } /** * 列表区空态 —— 侧栏窄,用紧凑版:软色圆里一枚图标 + 主句 + 一小段说明。 * 与主面板的 `EmptyState`(整屏光环)同一语气,尺寸各归其位。 */ export function ListEmpty({ icon, title, children, }: { icon: string; title?: string; children: ReactNode; }) { return (
{title !== undefined && {title}}

{children}

); } /** 中栏文档卡的滚动容器 */ export function DocPanel({ scrollKey, children }: { scrollKey: string; children: ReactNode }) { return (
{children}
); } /** * 说明带 —— 原型里用来点明规范口径的那条浅色横条。 * 保留它不是为了好看:这些句子是「为什么这么设计」的现场注解。 */ export function NoteBand({ children }: { children: ReactNode }) { return (
{children}
); } /** * 空态整屏 —— 层叠光环 + 图标 + 主副文 + 提示囊(自原型 `EmptyState` 平移)。 * * **空态是产品的常态,不是缺陷**:文案一律从用人的角度写「你手上现在没有什么」, * 而不是从开发角度写「这块还没做」—— 后者是给自己看的,前者才是用户要的那句话。 * * 三个色调分的是**三种空**,不是三种好看: * - `ok` 办完了、清空了 —— 这是好事(待批清空、无未读) * - `brand` 还没发生,但通路是通的 —— 等着就会有(还没有任务、还没有成果) * - `muted` 通路还没接 —— 等下去也不会有内容。灰着,别让人白等 */ export function EmptyState({ ico, tone, title, desc, hint, }: { ico: string; tone: 'ok' | 'brand' | 'muted'; title: string; desc: string; hint?: string | undefined; }) { return (
{title}

{desc}

{hint !== undefined && ( {hint} )}
); }